Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add install screen to samples #528

Merged
merged 7 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions Samples/Common/Package.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// swift-tools-version:5.3
// swift-tools-version:5.7

import PackageDescription

let package = Package(
name: "KSCrashSamplesCommon",
platforms: [
.iOS(.v14),
.tvOS(.v14),
.watchOS(.v7),
.macOS(.v11),
.iOS(.v15),
.tvOS(.v15),
.watchOS(.v8),
.macOS(.v13),
],
products: [
.library(
Expand All @@ -26,13 +26,15 @@ let package = Package(
],
dependencies: [
.package(path: "../.."),
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
],
targets: [
.target(
name: "LibraryBridge",
dependencies: [
.product(name: "Recording", package: "KSCrash"),
.product(name: "Reporting", package: "KSCrash"),
.product(name: "Logging", package: "swift-log"),
]
),
.target(
Expand All @@ -43,6 +45,7 @@ let package = Package(
dependencies: [
.target(name: "LibraryBridge"),
.target(name: "CrashTriggers"),
.product(name: "Logging", package: "swift-log"),
]
)
]
Expand Down
161 changes: 161 additions & 0 deletions Samples/Common/Sources/LibraryBridge/InstallBridge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
//
// InstallBridge.swift
//
// Created by Nikolay Volosatov on 2024-07-07.
//
// Copyright (c) 2012 Karl Stenerud. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall remain in place
// in this source code.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import Foundation
import Combine
import SwiftUI
import KSCrashRecording
import Logging

public enum BasePath: String, CaseIterable {
case `default`
case cache
case applicationSupport
}

public class InstallBridge: ObservableObject {
public typealias MonitorType = KSCrashRecording.MonitorType

public enum InstallationError: Error, LocalizedError {
case kscrashError(String)
case unexpectedError(String)
case alreadyInstalled

public var errorDescription: String? {
switch self {
case .kscrashError(let message), .unexpectedError(let message):
return message
case .alreadyInstalled:
return "KSCrash is already installed"
}
}
}

private static let logger = Logger(label: "InstallBridge")

private static func setBasePath(_ value: BasePath) {
let basePath = value.basePaths.first.flatMap { $0 + "/KSCrash" }
Self.logger.info("Setting KSCrash base path to: \(basePath ?? "<default>")")
KSCrash.setBasePath(basePath)
}

private var config: KSCrashConfiguration
private var disposables = Set<AnyCancellable>()

@Published public var basePath: BasePath = .default
@Published public var installed: Bool = false
@Published public var error: InstallationError?

public init() {
config = .init()

$basePath
.removeDuplicates()
.sink(receiveValue: Self.setBasePath(_:))
.store(in: &disposables)
}

public func install() {
guard !installed else {
error = .alreadyInstalled
return
}

do {
try KSCrash.shared.install(with: config)
installed = true
} catch let error as KSCrashInstallError {
let message = error.localizedDescription
Self.logger.error("Failed to install KSCrash: \(message)")
self.error = .kscrashError(message)
} catch {
let message = error.localizedDescription
Self.logger.error("Unexpected error during KSCrash installation: \(message)")
self.error = .unexpectedError(message)
}
}
}

// An utility method to simplify binding of config fields
extension InstallBridge {
public func configBinding<T>(for keyPath: WritableKeyPath<KSCrashConfiguration, T>) -> Binding<T> {
.init { [config] in
config[keyPath: keyPath]
} set: { [weak self] val in
self?.objectWillChange.send()
self?.config[keyPath: keyPath] = val
}
}
}

// Monitor types are specified here
extension InstallBridge {
public static let allRawMonitorTypes: [(monitor: MonitorType, name: String, description: String)] = [
(.machException, "Mach Exception", "Low-level system exceptions"),
(.signal, "Signal", "UNIX-style signals indicating abnormal program termination"),
(.cppException, "C++ Exception", "Unhandled exceptions in C++ code"),
(.nsException, "NSException", "Unhandled Objective-C exceptions"),
(.mainThreadDeadlock, "Main Thread Deadlock", "Situations where the main thread becomes unresponsive"),
(.memoryTermination, "Memory Termination", "Termination due to excessive memory usage"),
(.zombie, "Zombie", "Attempts to access deallocated objects"),
(.userReported, "User Reported", "Custom crash reports"),
(.system, "System", "Additional system information added to reports"),
(.applicationState, "Application State", "Application lifecycle added to report"),
]

public static let allCompositeMonitorTypes: [(monitor: MonitorType, name: String)] = [
(.all, "All"),
(.fatal, "Fatal"),

(.productionSafe, "Production-safe"),
(.productionSafeMinimal, "Production-safe Minimal"),
(.experimental, "Experimental"),

(.required, "Required"),
(.optional, "Optional"),

(.debuggerSafe, "Debugger-safe"),
(.debuggerUnsafe, "Debugger-unsafe"),

(.asyncSafe, "Async-safe"),
(.asyncUnsafe, "Async-unsafe"),

(.manual, "Manual"),
]
}

extension BasePath {
var basePaths: [String] {
switch self {
case .default:
return []
case .cache:
return NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
case .applicationSupport:
return NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)
}
}
}
20 changes: 13 additions & 7 deletions Samples/Common/Sources/LibraryBridge/ReportingSample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,30 @@ import Foundation
import KSCrashRecording
import KSCrashFilters
import KSCrashSinks
import Logging

public class ReportingSample {
private static let logger = Logger(label: "ReportingSample")

public static func logToConsole() {
KSCrash.shared.sink = CrashReportSinkConsole.filter().defaultCrashReportFilterSet()
KSCrash.shared.sendAllReports { reports, isSuccess, error in
if isSuccess, let reports {
print("Logged \(reports.count) reports")
Self.logger.info("Logged \(reports.count) reports")
for (idx, report) in reports.enumerated() {
switch report {
case let stringReport as CrashReportString:
print("Report #\(idx) is a string (length is \(stringReport.value.count))")
Self.logger.info("Report #\(idx) is a string (length is \(stringReport.value.count))")
case let dictionaryReport as CrashReportDictionary:
print("Report #\(idx) is a dictionary (number of keys is \(dictionaryReport.value.count))")
Self.logger.info("Report #\(idx) is a dictionary (number of keys is \(dictionaryReport.value.count))")
case let dataReport as CrashReportData:
print("Report #\(idx) is a binary data (size is \(dataReport.value.count) bytes)")
Self.logger.info("Report #\(idx) is a binary data (size is \(dataReport.value.count) bytes)")
default:
print("Unknown report #\(idx): \(report.debugDescription ?? "?")")
Self.logger.warning("Unknown report #\(idx): \(report.debugDescription ?? "?")")
}
}
} else {
print("Failed to log reports: \(error?.localizedDescription ?? "")")
Self.logger.error("Failed to log reports: \(error?.localizedDescription ?? "")")
}
}
}
Expand Down Expand Up @@ -117,6 +120,8 @@ public class SampleFilter: NSObject, CrashReportFilter {
}

public class SampleSink: NSObject, CrashReportFilter {
private static let logger = Logger(label: "SampleSink")

public func filterReports(_ reports: [any CrashReport], onCompletion: (([any CrashReport]?, Bool, (any Error)?) -> Void)? = nil) {
for (idx, report) in reports.enumerated() {
guard let sampleReport = report as? SampleCrashReport else {
Expand All @@ -126,7 +131,8 @@ public class SampleSink: NSObject, CrashReportFilter {
"Crash report #\(idx):",
"\tCrashed thread #\(sampleReport.crashedThread.index):",
] + sampleReport.crashedThread.callStack.map { "\t\t\($0)" }
print(lines.joined(separator: "\n"))
let text = lines.joined(separator: "\n")
Self.logger.info("\(text)")
}
onCompletion?(reports, true, nil)
}
Expand Down
81 changes: 81 additions & 0 deletions Samples/Common/Sources/SampleUI/Components/MonitorTypeView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// MonitorTypeView.swift
//
// Created by Nikolay Volosatov on 2024-07-07.
//
// Copyright (c) 2012 Karl Stenerud. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall remain in place
// in this source code.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import Foundation
import SwiftUI
import LibraryBridge

struct MonitorTypeView: View {

@Binding var monitors: InstallBridge.MonitorType

private func monitorBinding(_ monitor: InstallBridge.MonitorType) -> Binding<Bool> {
return .init(get: {
monitors.contains(monitor)
}, set: { flag in
if flag {
monitors.insert(monitor)
} else {
monitors.remove(monitor)
}
})
}

var body: some View {
List {
Section(header: Text("Monitors")) {
ForEach(InstallBridge.allRawMonitorTypes, id: \.monitor.rawValue) { (monitor, name, description) in
Toggle(isOn: monitorBinding(monitor)) {
VStack(alignment: .leading) {
Text(name)
Text(description)
.font(.caption)
.foregroundStyle(Color.secondary)
}
}
}
}
Section(header: Text("Composite")) {
ForEach(InstallBridge.allCompositeMonitorTypes, id: \.name) { (monitor, name) in
HStack {
Text(name)
Spacer()
Group {
Button("+") { monitors.formUnion(monitor) }
.disabled(monitors.intersection(monitor) == monitor)
.tint(Color.green)
Button("-") { monitors.subtract(monitor) }
.disabled(monitors.intersection(monitor).isEmpty)
.tint(Color.red)
}
.buttonStyle(.bordered)
.font(.subheadline.monospaced())
}
}
}
}
}
}
Loading