|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the Swift.org open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors |
| 6 | +// Licensed under Apache License v2.0 with Runtime Library Exception |
| 7 | +// |
| 8 | +// See https://swift.org/LICENSE.txt for license information |
| 9 | +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| 10 | +// |
| 11 | +//===----------------------------------------------------------------------===// |
| 12 | + |
| 13 | +import ArgumentParser |
| 14 | +import Foundation |
| 15 | +import SKCore |
| 16 | + |
| 17 | +import struct TSCBasic.AbsolutePath |
| 18 | + |
| 19 | +public struct DiagnoseCommand: AsyncParsableCommand { |
| 20 | + public static var configuration: CommandConfiguration = CommandConfiguration( |
| 21 | + commandName: "diagnose", |
| 22 | + abstract: "Reduce sourcekitd crashes", |
| 23 | + shouldDisplay: false |
| 24 | + ) |
| 25 | + |
| 26 | + @Option( |
| 27 | + name: .customLong("request-file"), |
| 28 | + help: |
| 29 | + "Path to a sourcekitd request. If not specified, the command will look for crashed sourcekitd requests and have been logged to OSLog" |
| 30 | + ) |
| 31 | + var sourcekitdRequestPath: String? |
| 32 | + |
| 33 | + @Option( |
| 34 | + name: .customLong("os-log-history"), |
| 35 | + help: "If now request file is passed, how many minutes of OS Log history should be scraped for a crash." |
| 36 | + ) |
| 37 | + var osLogScrapeDuration: Int = 60 |
| 38 | + |
| 39 | + @Option( |
| 40 | + name: .customLong("sourcekitd"), |
| 41 | + help: |
| 42 | + "Path to sourcekitd.framework/sourcekitd. If not specified, the toolchain is found in the same way that sourcekit-lsp finds it" |
| 43 | + ) |
| 44 | + var sourcekitdOverride: String? |
| 45 | + |
| 46 | + #if canImport(Darwin) |
| 47 | + // Creating an NSPredicate from a string is not supported in corelibs-foundation. |
| 48 | + @Option( |
| 49 | + help: """ |
| 50 | + If the sourcekitd response matches this predicate, consider it as reproducing the issue. |
| 51 | + sourcekitd crashes are always considered as reproducers. |
| 52 | +
|
| 53 | + The predicate is an NSPredicate and `self` is the sourcekitd response. |
| 54 | + """ |
| 55 | + ) |
| 56 | + var predicate: String? |
| 57 | + #endif |
| 58 | + |
| 59 | + var sourcekitd: String? { |
| 60 | + get async throws { |
| 61 | + if let sourcekitdOverride { |
| 62 | + return sourcekitdOverride |
| 63 | + } |
| 64 | + |
| 65 | + let installPath = try AbsolutePath(validating: Bundle.main.bundlePath) |
| 66 | + let toolchainRegistry = ToolchainRegistry(installPath: installPath) |
| 67 | + return await toolchainRegistry.default?.sourcekitd?.pathString |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + /// Request infos of crashes that should be diagnosed. |
| 72 | + func requestInfos() throws -> [(name: String, info: RequestInfo)] { |
| 73 | + if let sourcekitdRequestPath { |
| 74 | + let request = try String(contentsOfFile: sourcekitdRequestPath) |
| 75 | + return [(sourcekitdRequestPath, try RequestInfo(request: request))] |
| 76 | + } |
| 77 | + #if canImport(OSLog) |
| 78 | + return try OSLogScraper(searchDuration: TimeInterval(osLogScrapeDuration * 60)).getCrashedRequests() |
| 79 | + #else |
| 80 | + throw ReductionError("--request-file must be specified on all platforms other than macOS") |
| 81 | + #endif |
| 82 | + } |
| 83 | + |
| 84 | + public init() {} |
| 85 | + |
| 86 | + public func run() async throws { |
| 87 | + guard let sourcekitd = try await sourcekitd else { |
| 88 | + throw ReductionError("Unable to find sourcekitd.framework") |
| 89 | + } |
| 90 | + |
| 91 | + for (name, requestInfo) in try requestInfos() { |
| 92 | + print("-- Diagnosing \(name)") |
| 93 | + do { |
| 94 | + var requestInfo = requestInfo |
| 95 | + var nspredicate: NSPredicate? = nil |
| 96 | + #if canImport(Darwin) |
| 97 | + if let predicate { |
| 98 | + nspredicate = NSPredicate(format: predicate) |
| 99 | + } |
| 100 | + #endif |
| 101 | + let executor = SourceKitRequestExecutor( |
| 102 | + sourcekitd: URL(fileURLWithPath: sourcekitd), |
| 103 | + reproducerPredicate: nspredicate |
| 104 | + ) |
| 105 | + let fileReducer = FileReducer(sourcekitdExecutor: executor) |
| 106 | + requestInfo = try await fileReducer.run(initialRequestInfo: requestInfo) |
| 107 | + |
| 108 | + let commandLineReducer = CommandLineArgumentReducer(sourcekitdExecutor: executor) |
| 109 | + requestInfo = try await commandLineReducer.run(initialRequestInfo: requestInfo) |
| 110 | + |
| 111 | + let reproducerBundle = try makeReproducerBundle(for: requestInfo) |
| 112 | + |
| 113 | + print("----------------------------------------") |
| 114 | + print( |
| 115 | + "Reduced SourceKit crash and created a bundle that contains information to reproduce the issue at the following path." |
| 116 | + ) |
| 117 | + print("Please file an issue at https://github.com/apple/sourcekit-lsp/issues/new and attach this bundle") |
| 118 | + print() |
| 119 | + print(reproducerBundle.path) |
| 120 | + |
| 121 | + // We have found a reproducer. Stop. Looking further probably won't help because other crashes are likely the same cause. |
| 122 | + return |
| 123 | + } catch { |
| 124 | + // Reducing this request failed. Continue reducing the next one, maybe that one succeeds. |
| 125 | + print(error) |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + print("No reducible crashes found") |
| 130 | + throw ExitCode(1) |
| 131 | + } |
| 132 | +} |
0 commit comments