diff --git a/CHANGELOG.md b/CHANGELOG.md index eca7fd3f6..f3944b9b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,15 @@ ##### Breaking -* None. +* Change `SwiftDocs.init(file:arguments:)` to + `SwiftDocs.init?(file:arguments:)` + [Norio Nomura](https://github.com/norio-nomura) ##### Enhancements -* None. +* Add `Request.failableSend()` that can handle SourceKitService crashes. + `sourcekitten doc` does not stop when SourceKitService crashes. + [Norio Nomura](https://github.com/norio-nomura) ##### Bug Fixes diff --git a/Source/SourceKittenFramework/File.swift b/Source/SourceKittenFramework/File.swift index 1e36df587..da1e62239 100644 --- a/Source/SourceKittenFramework/File.swift +++ b/Source/SourceKittenFramework/File.swift @@ -169,8 +169,8 @@ public final class File { var dictionary = dictionary let offsetMap = generateOffsetMap(documentedTokenOffsets, dictionary: dictionary) for offset in offsetMap.keys.reverse() { // Do this in reverse to insert the doc at the correct offset - let response = processDictionary(Request.sendCursorInfoRequest(cursorInfoRequest, atOffset: Int64(offset))!, cursorInfoRequest: nil, syntaxMap: syntaxMap) - if let kind = SwiftDocKey.getKind(response), + if let response = Request.sendCursorInfoRequest(cursorInfoRequest, atOffset: Int64(offset)).map({ processDictionary($0, cursorInfoRequest: nil, syntaxMap: syntaxMap) }), + kind = SwiftDocKey.getKind(response), _ = SwiftDeclarationKind(rawValue: kind), parentOffset = offsetMap[offset].flatMap({ Int64($0) }), inserted = insertDoc(response, parent: dictionary, offset: parentOffset) { diff --git a/Source/SourceKittenFramework/Request.swift b/Source/SourceKittenFramework/Request.swift index f20660bee..3c7b8473f 100644 --- a/Source/SourceKittenFramework/Request.swift +++ b/Source/SourceKittenFramework/Request.swift @@ -91,6 +91,10 @@ private func fromSourceKit(sourcekitObject: sourcekitd_variant_t) -> SourceKitRe /// dispatch_once_t token used to only initialize SourceKit once per session. private var sourceKitInitializationToken: dispatch_once_t = 0 +/// dispatch_semaphore_t used when waiting for sourcekitd to be restored. +private var sourceKitWaitingRestoredSemaphore = dispatch_semaphore_create(0) +private let sourceKitWaitingRestoredTimeout = Int64(10 * NSEC_PER_SEC) + /// SourceKit UID to String map. private var uidStringMap = [sourcekitd_uid_t: String]() @@ -241,7 +245,7 @@ public enum Request { return nil } sourcekitd_request_dictionary_set_int64(request, sourcekitd_uid_get_from_cstr(SwiftDocKey.Offset.rawValue), offset) - return Request.CustomRequest(request).send() + return try? Request.CustomRequest(request).failableSend() } /** @@ -257,6 +261,72 @@ public enum Request { defer { sourcekitd_response_dispose(response) } return fromSourceKit(sourcekitd_response_get_value(response)) as! [String: SourceKitRepresentable] } + + /// A enum representation of SOURCEKITD_ERROR_* + public enum Error: ErrorType, CustomStringConvertible { + case ConnectionInterrupted(String?) + case Invalid(String?) + case Failed(String?) + case Cancelled(String?) + case Unknown(String?) + + /// A textual representation of `self`. + public var description: String { + return getDescription() ?? "no description" + } + + private func getDescription() -> String? { + switch self { + case .ConnectionInterrupted(let string): return string + case .Invalid(let string): return string + case .Failed(let string): return string + case .Cancelled(let string): return string + case .Unknown(let string): return string + } + } + + private init(response: sourcekitd_response_t) { + let description = String(UTF8String: sourcekitd_response_error_get_description(response)) + switch sourcekitd_response_error_get_kind(response) { + case SOURCEKITD_ERROR_CONNECTION_INTERRUPTED: self = .ConnectionInterrupted(description) + case SOURCEKITD_ERROR_REQUEST_INVALID: self = .Invalid(description) + case SOURCEKITD_ERROR_REQUEST_FAILED: self = .Failed(description) + case SOURCEKITD_ERROR_REQUEST_CANCELLED: self = .Cancelled(description) + default: self = .Unknown(description) + } + } + } + + /** + Sends the request to SourceKit and return the response as an [String: SourceKitRepresentable]. + + - returns: SourceKit output as a dictionary. + - throws: Request.Error on fail () + */ + public func failableSend() throws -> [String: SourceKitRepresentable] { + dispatch_once(&sourceKitInitializationToken) { + sourcekitd_initialize() + sourcekitd_set_notification_handler() { response in + if !sourcekitd_response_is_error(response) { + fflush(stdout) + fputs("sourcekitten: connection to SourceKitService restored!\n", stderr) + dispatch_semaphore_signal(sourceKitWaitingRestoredSemaphore) + } + sourcekitd_response_dispose(response) + } + } + let response = sourcekitd_send_request_sync(sourcekitObject) + defer { sourcekitd_response_dispose(response) } + if sourcekitd_response_is_error(response) { + let error = Request.Error(response: response) + if case .ConnectionInterrupted = error { + dispatch_semaphore_wait(sourceKitWaitingRestoredSemaphore, + dispatch_time(DISPATCH_TIME_NOW, sourceKitWaitingRestoredTimeout)) + } + throw error + } + return fromSourceKit(sourcekitd_response_get_value(response)) as! [String: SourceKitRepresentable] + } } // MARK: CustomStringConvertible diff --git a/Source/SourceKittenFramework/SwiftDocs.swift b/Source/SourceKittenFramework/SwiftDocs.swift index 72e0d97f2..43c607582 100644 --- a/Source/SourceKittenFramework/SwiftDocs.swift +++ b/Source/SourceKittenFramework/SwiftDocs.swift @@ -25,12 +25,19 @@ public struct SwiftDocs { - parameter file: Swift file to document. - parameter arguments: compiler arguments to pass to SourceKit. */ - public init(file: File, arguments: [String]) { - self.init( - file: file, - dictionary: Request.EditorOpen(file).send(), - cursorInfoRequest: Request.cursorInfoRequestForFilePath(file.path, arguments: arguments) - ) + public init?(file: File, arguments: [String]) { + do { + self.init( + file: file, + dictionary: try Request.EditorOpen(file).failableSend(), + cursorInfoRequest: Request.cursorInfoRequestForFilePath(file.path, arguments: arguments) + ) + } catch let error as Request.Error { + fputs(error.description, stderr) + return nil + } catch { + return nil + } } /** diff --git a/Source/SourceKittenFrameworkTests/SwiftDocsTests.swift b/Source/SourceKittenFrameworkTests/SwiftDocsTests.swift index 2451b4374..b3b77537e 100644 --- a/Source/SourceKittenFrameworkTests/SwiftDocsTests.swift +++ b/Source/SourceKittenFrameworkTests/SwiftDocsTests.swift @@ -30,7 +30,7 @@ func compareJSONStringWithFixturesName(name: String, jsonString: String) { func compareDocsWithFixturesName(name: String) { let swiftFilePath = fixturesDirectory + name + ".swift" - let docs = SwiftDocs(file: File(path: swiftFilePath)!, arguments: ["-j4", swiftFilePath]) + let docs = SwiftDocs(file: File(path: swiftFilePath)!, arguments: ["-j4", swiftFilePath])! let escapedFixturesDirectory = fixturesDirectory.stringByReplacingOccurrencesOfString("/", withString: "\\/") let comparisonString = String(docs).stringByReplacingOccurrencesOfString(escapedFixturesDirectory, withString: "") diff --git a/Source/sourcekitten/DocCommand.swift b/Source/sourcekitten/DocCommand.swift index e06766db7..cd842d1e4 100644 --- a/Source/sourcekitten/DocCommand.swift +++ b/Source/sourcekitten/DocCommand.swift @@ -43,8 +43,8 @@ struct DocCommand: CommandType { return .Failure(.InvalidArgument(description: "at least 5 arguments are required when using `--single-file`")) } let sourcekitdArguments = Array(args[4..() -registry.register(CompleteCommand()) -registry.register(DocCommand()) -registry.register(SyntaxCommand()) -registry.register(StructureCommand()) -registry.register(VersionCommand()) +// `sourcekitd_set_notification_handler()` set the handler to be executed on main thread queue. +// So, we vacate main thread to `dispatch_main()`. +dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { + let registry = CommandRegistry() + registry.register(CompleteCommand()) + registry.register(DocCommand()) + registry.register(SyntaxCommand()) + registry.register(StructureCommand()) + registry.register(VersionCommand()) -let helpCommand = HelpCommand(registry: registry) -registry.register(helpCommand) + let helpCommand = HelpCommand(registry: registry) + registry.register(helpCommand) -registry.main(defaultVerb: "help") { error in - fputs("\(error)\n", stderr) + registry.main(defaultVerb: "help") { error in + fputs("\(error)\n", stderr) + } } + +dispatch_main()