1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
|
//===--------------------- SourceKitdClient.swift -------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
// This file provides a wrapper of SourceKitd service.
//===----------------------------------------------------------------------===//
import sourcekitd
import Dispatch
/// An empty object to generate unique `ObjectIdentifier`s.
fileprivate class Object {}
public class SourceKitdService {
enum State {
case running
case interrupted
case semaDisabled
}
/// The queue that makes sure only one request is executed at a time
private let requestQueue = DispatchQueue(label: "SourceKitdService.requestQueue", qos: .userInitiated)
/// The queue that guards access to the `state` and `stateChangeHandlers` variables
private let stateQueue = DispatchQueue(label: "SourceKitdService.stateQueue", qos: .userInitiated)
private var state: State = .running {
didSet {
dispatchPrecondition(condition: .onQueue(stateQueue))
for handler in stateChangeHandlers.values {
handler()
}
}
}
/// Handlers to be executed whenever the `state` changes.
private var stateChangeHandlers: [ObjectIdentifier: () -> Void] = [:]
private let printRequests: Bool
public init(printRequests: Bool) {
self.printRequests = printRequests
initializeService()
}
private func sendRequestSync(_ req: SourceKitdRequest) -> sourcekitd_response_t! {
if printRequests {
print(req.description)
}
return sourcekitd_send_request_sync(req.rawRequest)
}
/// Set up a new SourceKit service instance.
private func initializeService() {
sourcekitd_initialize()
sourcekitd_set_notification_handler { [self] resp in
let response = SourceKitdResponse(resp: resp!)
stateQueue.async {
if self.state == .interrupted {
self.state = .semaDisabled
// sourcekitd came back online. Poke it to restore semantic
// functionality. Intentionally don't execute this request on the
// request queue because request order doesn't matter for this
// pseudo-document and we want it to be executed immediately, even if
// the request queue is blocked.
let request = SourceKitdRequest(uid: .request_CursorInfo)
request.addParameter(.key_SourceText, value: "")
_ = self.sendRequestSync(request)
}
if response.isConnectionInterruptionError {
self.state = .interrupted
} else if response.notificationType == .semaDisabledNotification {
self.state = .semaDisabled
} else if response.notificationType == .semaEnabledNotification {
self.state = .running
}
}
}
}
deinit {
sourcekitd_shutdown()
}
/// Crash the service. This is a workaround to set up a new service in case
/// we time out waiting for a request response and we want to handle it.
/// Replace by proper cancellation once we have cancellation support in
/// SourceKit.
public func crash() {
let request = SourceKitdRequest(uid: .request_CrashWithExit)
_ = sendRequestSync(request)
stateQueue.sync {
self.state = .interrupted
}
}
/// Execute `callback` one this service is in `desiredState`
private func waitForState(_ desiredState: State, callback: @escaping () -> Void) {
stateQueue.async { [self] in
if state == desiredState {
callback()
} else {
let identifier = ObjectIdentifier(Object())
stateChangeHandlers[identifier] = { [self] in
dispatchPrecondition(condition: .onQueue(stateQueue))
if state == desiredState {
callback()
stateChangeHandlers[identifier] = nil
}
}
}
}
}
/// Block the current thread until this service is in `desiredState`.
private func blockUntilState(_ desiredState: State) {
let semaphore = DispatchSemaphore(value: 0)
waitForState(desiredState) {
semaphore.signal()
}
semaphore.wait()
}
/// Send a request synchronously with a handler for its response.
/// - Parameter request: The request to send.
/// - Returns: The response from the sourcekitd service.
public func sendSyn(request: SourceKitdRequest) -> SourceKitdResponse {
return requestQueue.sync {
blockUntilState(.running)
return SourceKitdResponse(resp: sendRequestSync(request))
}
}
/// Send a request asynchronously with a handler for its response.
/// - Parameter request: The request to send.
/// - Parameter handler: The handler for the response in the future.
public func send(request: SourceKitdRequest,
handler: @escaping (SourceKitdResponse) -> ()) {
requestQueue.async { [self] in
blockUntilState(.running)
let response = SourceKitdResponse(resp: sendRequestSync(request))
if response.isConnectionInterruptionError {
// Set the state into the interrupted state now. We will also catch this
// in the notification handler but that has some delay and we might be
// scheduling new requests before the state is set to `interrupted`.
stateQueue.sync {
self.state = .interrupted
}
}
handler(response)
}
}
}
|