diff --git a/.env.example b/.env.example index ac01b9856..0bca23789 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ GH_TOKEN=xxxx ENCRYPTION_SECRET=xxxx CERT_PASSWORD=xxxx +SENTRY_AUTH_TOKEN=xxx +SENTRY_DSN=xxxx \ No newline at end of file diff --git a/App.xcodeproj/project.pbxproj b/App.xcodeproj/project.pbxproj index 40eaa3595..c594ed862 100644 --- a/App.xcodeproj/project.pbxproj +++ b/App.xcodeproj/project.pbxproj @@ -18,6 +18,10 @@ B915ED662063B18B004B6630 /* xcproj.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B915ED672063B18B004B6630 /* xcproj.framework */; }; B91834BF207CBCE6008935B4 /* ProjectDescription.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B91834BE207CBCE6008935B4 /* ProjectDescription.framework */; }; B91834C0207CBCFE008935B4 /* ProjectDescription.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B91834BE207CBCE6008935B4 /* ProjectDescription.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B918A22A20935EC800E64FBE /* Sentry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B918A22920935EC800E64FBE /* Sentry.framework */; }; + B918A22B20935ECF00E64FBE /* Sentry.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B918A22920935EC800E64FBE /* Sentry.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B918A22C20935ED700E64FBE /* Sentry.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B918A22920935EC800E64FBE /* Sentry.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B918A22E20935EE900E64FBE /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B918A22D20935EE900E64FBE /* ErrorHandler.swift */; }; B91FF331206AB4E6005EA520 /* xcbuddy in Copy CLI */ = {isa = PBXBuildFile; fileRef = B915ECF6206395DA004B6630 /* xcbuddy */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; B92BF9FE2075608B00EE4EBD /* Data+TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B92BF9FD2075608B00EE4EBD /* Data+TestData.swift */; }; B94C7FC12062B8A8009BF596 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B94C7FB72062B8A7009BF596 /* Assets.xcassets */; }; @@ -178,6 +182,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + B918A22B20935ECF00E64FBE /* Sentry.framework in Embed Frameworks */, B9E2DCB22087757D0061DF86 /* PathKit.framework in Embed Frameworks */, B9E2DCA520876F660061DF86 /* Utility.framework in Embed Frameworks */, B9FB2DC82086516A00BC2FB3 /* clibc.framework in Embed Frameworks */, @@ -198,6 +203,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + B918A22C20935ED700E64FBE /* Sentry.framework in Embed Frameworks */, B9E2DCB3208775860061DF86 /* PathKit.framework in Embed Frameworks */, B9E2DCAC208775160061DF86 /* clibc.framework in Embed Frameworks */, B9E2DCAD208775160061DF86 /* SPMLibc.framework in Embed Frameworks */, @@ -225,6 +231,8 @@ B915ED4D2063B04C004B6630 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B915ED672063B18B004B6630 /* xcproj.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = xcproj.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B91834BE207CBCE6008935B4 /* ProjectDescription.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ProjectDescription.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B918A22920935EC800E64FBE /* Sentry.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Sentry.framework; sourceTree = ""; }; + B918A22D20935EE900E64FBE /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; B9287D0520808FFF002DEFEE /* BuildConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildConfigurationTests.swift; sourceTree = ""; }; B92BF8152073E66200EE4EBD /* MockUpdateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUpdateController.swift; sourceTree = ""; }; B92BF9F0207559E600EE4EBD /* MockGraphLoaderCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGraphLoaderCache.swift; sourceTree = ""; }; @@ -344,6 +352,7 @@ B9E2DCA420876F490061DF86 /* Utility.framework in Frameworks */, B9FB2DBF2086506E00BC2FB3 /* Basic.framework in Frameworks */, B91834BF207CBCE6008935B4 /* ProjectDescription.framework in Frameworks */, + B918A22A20935EC800E64FBE /* Sentry.framework in Frameworks */, B9013DD5206FEEDF007B1A34 /* Sparkle.framework in Frameworks */, B915ED662063B18B004B6630 /* xcproj.framework in Frameworks */, ); @@ -371,6 +380,7 @@ B915EC392062EC97004B6630 /* Frameworks */ = { isa = PBXGroup; children = ( + B918A22920935EC800E64FBE /* Sentry.framework */, B9E2DCAF2087756D0061DF86 /* PathKit.framework */, B9FB2DC22086507700BC2FB3 /* clibc.framework */, B9FB2DC42086507700BC2FB3 /* SPMLibc.framework */, @@ -575,6 +585,7 @@ B9553DE92090690000050311 /* Shell.swift */, B9553DF02092181500050311 /* Context.swift */, B9553DF220921AF300050311 /* ResourceLocator.swift */, + B918A22D20935EE900E64FBE /* ErrorHandler.swift */, ); path = Utils; sourceTree = ""; @@ -770,6 +781,7 @@ B9FDBF33206423460010BC33 /* Embed Frameworks */, B9B80AB0206AAFC30057482B /* Copy CLI */, B91834CF207CEB00008935B4 /* Copy ProjectDescription Framework */, + B918A22F209365E900E64FBE /* Upload symbols to sentry */, ); buildRules = ( ); @@ -875,6 +887,20 @@ shellPath = /bin/sh; shellScript = "${SRCROOT}/scripts/copy-framework.sh ProjectDescription.framework"; }; + B918A22F209365E900E64FBE /* Upload symbols to sentry */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Upload symbols to sentry"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which sentry-cli >/dev/null; then\nexport SENTRY_ORG=xcbuddy\nexport SENTRY_PROJECT=app\nERROR=$(bin/sentry-cli upload-dsym 2>&1 >/dev/null)\nif [ ! $? -eq 0 ]; then\necho \"warning: sentry-cli - $ERROR\"\nfi\nelse\necho \"warning: sentry-cli not installed, download from https://github.com/getsentry/sentry-cli/releases\"\nfi"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -890,6 +916,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B918A22E20935EE900E64FBE /* ErrorHandler.swift in Sources */, B9B629AC20864E3A00EE9E07 /* FileHandler.swift in Sources */, B9553DEA2090690000050311 /* Shell.swift in Sources */, B95895F3208A2ACF00F00ACF /* ProjectGenerator.swift in Sources */, diff --git a/App/xcbuddykit/Info.plist b/App/xcbuddykit/Info.plist index 7c270cac5..d1fdc6f5b 100644 --- a/App/xcbuddykit/Info.plist +++ b/App/xcbuddykit/Info.plist @@ -24,5 +24,7 @@ Copyright © 2018 ppinera.es. All rights reserved. NSPrincipalClass + SENTRY_DSN + $(SENTRY_DSN) diff --git a/App/xcbuddykit/Sources/Loader/GraphManifestLoader.swift b/App/xcbuddykit/Sources/Loader/GraphManifestLoader.swift index 202663338..c110b7903 100644 --- a/App/xcbuddykit/Sources/Loader/GraphManifestLoader.swift +++ b/App/xcbuddykit/Sources/Loader/GraphManifestLoader.swift @@ -61,7 +61,7 @@ class GraphManifestLoader: GraphManifestLoading { throw GraphManifestLoaderError.swiftNotFound } let swiftPath = AbsolutePath(swiftOutput) - let manifestFrameworkPath = try context.resourceLocator.projectDescription(context: context) + let manifestFrameworkPath = try context.resourceLocator.projectDescription() let jsonString: String! = try context.shell.run(swiftPath.asString, "-F", manifestFrameworkPath.parentDirectory.asString, "-framework", "ProjectDescription", path.asString, "--dump").chuzzle() if jsonString == nil { throw GraphManifestLoaderError.unexpectedOutput(path) diff --git a/App/xcbuddykit/Sources/Utils/ErrorHandler.swift b/App/xcbuddykit/Sources/Utils/ErrorHandler.swift new file mode 100644 index 000000000..3fa3c8734 --- /dev/null +++ b/App/xcbuddykit/Sources/Utils/ErrorHandler.swift @@ -0,0 +1,87 @@ +import Foundation +import Sentry + +/// Error handling protocol. +protocol ErrorHandling: AnyObject { + /// It should be called when a fatal error happens. Depending on the error it + /// prints, and reports the error to Sentry. + /// + /// - Parameter error: error. + func fatal(error: FatalError) +} + +/// Fatal errors that can be thrown at any point of the execution. +/// +/// - abort: used when something unexpected happens and the user should be alerted. +/// - bug: like abort, but it also reports the event to Sentry. +/// - abortSilent: like abort, but it doesn't print anything in the console. +/// - bugSilent: like bug, but it doesn't print anything in the console. +enum FatalError: Error { + case abort(Error & CustomStringConvertible) + case bug(Error & CustomStringConvertible) + case abortSilent(Error) + case bugSilent(Error) + + /// Returns the error description + var description: String? { + switch self { + case let .abort(error): + return error.description + case let .bug(error): + return error.description + default: + return nil + } + } + + /// Returns a bug to be reported. + var bug: Error? { + switch self { + case let .bug(error): return error + case let .bugSilent(error): return error + default: return nil + } + } +} + +/// Error handler. +final class ErrorHandler: ErrorHandling { + /// Printer. + let printer: Printing + + /// Sentry client. + let client: Client? + + /// Initializes the error handler with its attributes. + /// + /// - Parameter printer: printer. + init(printer: Printing = Printer()) { + if let sentryDsn = Bundle(for: ErrorHandler.self).infoDictionary?["SENTRY_DSN"] as? String { + client = try! Client(dsn: sentryDsn) + try! client?.startCrashHandler() + } else { + client = nil + } + self.printer = printer + } + + /// It should be called when a fatal error happens. Depending on the error it + /// prints, and reports the error to Sentry. + /// + /// - Parameter error: error. + func fatal(error: FatalError) { + if let description = error.description { + printer.print(errorMessage: description) + } + if let bug = error.bug { + let event = Event(level: .debug) + event.message = bug.localizedDescription + let semaphore = DispatchSemaphore(value: 0) + client?.send(event: event) { _ in + semaphore.signal() + } + semaphore.wait() + } + exit(1) + } +} diff --git a/App/xcbuddykit/Sources/Utils/ResourceLocator.swift b/App/xcbuddykit/Sources/Utils/ResourceLocator.swift index 6a019cd3b..3e5b9fbc0 100644 --- a/App/xcbuddykit/Sources/Utils/ResourceLocator.swift +++ b/App/xcbuddykit/Sources/Utils/ResourceLocator.swift @@ -8,14 +8,14 @@ protocol ResourceLocating: AnyObject { /// - Parameter context: context. /// - Returns: ProjectDescription.framework path. /// - Throws: an error if the framework cannot be found. - func projectDescription(context: Contexting) throws -> AbsolutePath + func projectDescription() throws -> AbsolutePath /// Returns the CLI path. /// /// - Parameter context: context. /// - Returns: path to the xcbuddy CLI. /// - Throws: an error if the CLI cannot be found. - func cliPath(context: Contexting) throws -> AbsolutePath + func cliPath() throws -> AbsolutePath } /// Resource locating error. @@ -40,18 +40,27 @@ enum ResourceLocatingError: Error, CustomStringConvertible, Equatable { /// Resource locator. final class ResourceLocator: ResourceLocating { + /// File handler. + private let fileHandler: FileHandling + + /// Initializes the locator with its attributes. + /// + /// - Parameter fileHandler: file handler. + init(fileHandler: FileHandling = FileHandler()) { + self.fileHandler = fileHandler + } + /// Returns the ProjectDescription.framework path. /// - /// - Parameter context: context. /// - Returns: ProjectDescription.framework path. /// - Throws: an error if the framework cannot be found. - func projectDescription(context: Contexting) throws -> AbsolutePath { + func projectDescription() throws -> AbsolutePath { let frameworkName = "ProjectDescription.framework" let xcbuddyKitPath = AbsolutePath(Bundle(for: GraphManifestLoader.self).bundleURL.path) let parentPath = xcbuddyKitPath.parentDirectory let pathInProducts = parentPath.appending(component: frameworkName) // Built products directory - if context.fileHandler.exists(pathInProducts) { + if fileHandler.exists(pathInProducts) { return pathInProducts } // Frameworks directory inside the app bundle. @@ -63,7 +72,7 @@ final class ResourceLocator: ResourceLocating { throw ResourceLocatingError.notFound(frameworkName) } let frameworkPath = AbsolutePath(frameworksPath).appending(component: frameworkName) - if !context.fileHandler.exists(frameworkPath) { + if !fileHandler.exists(frameworkPath) { throw ResourceLocatingError.notFound(frameworkName) } return frameworkPath @@ -71,16 +80,15 @@ final class ResourceLocator: ResourceLocating { /// Returns the CLI path. /// - /// - Parameter context: context. /// - Returns: path to the xcbuddy CLI. /// - Throws: an error if the CLI cannot be found. - func cliPath(context: Contexting) throws -> AbsolutePath { + func cliPath() throws -> AbsolutePath { let toolName = "xcbuddy" let xcbuddyKitPath = AbsolutePath(Bundle(for: GraphManifestLoader.self).bundleURL.path) let parentPath = xcbuddyKitPath.parentDirectory let pathInProducts = parentPath.appending(component: toolName) // Built products directory - if context.fileHandler.exists(pathInProducts) { + if fileHandler.exists(pathInProducts) { return pathInProducts } // Frameworks directory inside the app bundle. @@ -92,7 +100,7 @@ final class ResourceLocator: ResourceLocating { throw ResourceLocatingError.notFound(toolName) } let toolPath = AbsolutePath(frameworksPath).appending(component: toolName) - if !context.fileHandler.exists(toolPath) { + if !fileHandler.exists(toolPath) { throw ResourceLocatingError.notFound(toolName) } return toolPath diff --git a/App/xcbuddykit/Tests/Utils/MockRersourceLocator.swift b/App/xcbuddykit/Tests/Utils/MockRersourceLocator.swift index 17242ac13..60bea1a6d 100644 --- a/App/xcbuddykit/Tests/Utils/MockRersourceLocator.swift +++ b/App/xcbuddykit/Tests/Utils/MockRersourceLocator.swift @@ -3,17 +3,17 @@ import Foundation final class MockResourceLocator: ResourceLocating { var projectDescriptionCount: UInt = 0 - var projectDescriptionStub: ((Contexting) throws -> AbsolutePath)? + var projectDescriptionStub: (() throws -> AbsolutePath)? var cliPathCount: UInt = 0 - var cliPathStub: ((Contexting) throws -> AbsolutePath)? + var cliPathStub: (() throws -> AbsolutePath)? - func projectDescription(context: Contexting) throws -> AbsolutePath { + func projectDescription() throws -> AbsolutePath { projectDescriptionCount += 1 - return try projectDescriptionStub?(context) ?? AbsolutePath("/") + return try projectDescriptionStub?() ?? AbsolutePath("/") } - func cliPath(context: Contexting) throws -> AbsolutePath { + func cliPath() throws -> AbsolutePath { cliPathCount += 1 - return try cliPathStub?(context) ?? AbsolutePath("/") + return try cliPathStub?() ?? AbsolutePath("/") } } diff --git a/bin/sentry-cli b/bin/sentry-cli new file mode 100755 index 000000000..63d67f710 --- /dev/null +++ b/bin/sentry-cli @@ -0,0 +1,26 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ + +'use strict'; + +const cli = require('../js'); +const child = require('child_process').spawn(cli.getPath(), process.argv.slice(2), { + stdio: 'inherit', +}); + +child.on('error', err => { + console.error('error: failed to invoke sentry-cli'); + console.error(err.stack); +}); + +child.on('exit', code => { + process.exit(code); +}); + +process.on('SIGTERM', () => { + child.kill('SIGTERM'); +}); + +process.on('SIGINT', () => { + child.kill('SIGINT'); +});