mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2026-03-26 06:00:07 +01:00
148 lines
5.8 KiB
Swift
148 lines
5.8 KiB
Swift
//
|
|
// CrashReporter.swift
|
|
// PHP Monitor
|
|
//
|
|
// Created by Nico Verbruggen on 01/11/2025.
|
|
// Copyright © 2025 Nico Verbruggen. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import CrashReporter
|
|
import NVAlert
|
|
import AppKit
|
|
|
|
class CrashReporter {
|
|
|
|
/**
|
|
Initializes the crash reporting toolkit. Keep in mind that this crash reporter only keeps track of crashes,
|
|
it does not automatically send information. I have my own API for my crash report ingest system.
|
|
*/
|
|
static func initialize() {
|
|
if CrashReporter.isDebuggerAttached() {
|
|
Log.err("[CrashReporter] The debugger is attached, won't start crash reporting.")
|
|
return
|
|
}
|
|
|
|
let config = PLCrashReporterConfig(signalHandlerType: .mach, symbolicationStrategy: [])
|
|
|
|
guard let crashReporter = PLCrashReporter(configuration: config) else {
|
|
Log.err("[CrashReporter] Could not create an instance of PLCrashReporter.")
|
|
return
|
|
}
|
|
|
|
do {
|
|
try crashReporter.enableAndReturnError()
|
|
} catch let error {
|
|
Log.err("[CrashReporter] Could not enable crash reporter: \(error). Crashes will not be reported.")
|
|
}
|
|
|
|
if crashReporter.hasPendingCrashReport() {
|
|
Task { @MainActor in
|
|
CrashReporter.requestSendingCrashReport(crashReporter)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
If a pending crash report can be sent, show an alert to the user.
|
|
*/
|
|
@MainActor static func requestSendingCrashReport(_ crashReporter: PLCrashReporter) {
|
|
do {
|
|
let data = try crashReporter.loadPendingCrashReportDataAndReturnError()
|
|
let report = try PLCrashReport(data: data)
|
|
|
|
if let text = PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS) {
|
|
// Ask the user to submit the crash report
|
|
let response = NVAlert().withInformation(
|
|
title: "crash_reporter.title".localized,
|
|
subtitle: "crash_reporter.subtitle".localized,
|
|
description: "crash_reporter.description".localized
|
|
)
|
|
.withTertiary(text: "", action: { _ in
|
|
try? text.write(toFile: "/tmp/pm_crash_log.txt", atomically: true, encoding: .utf8)
|
|
let fileUrl = URL(string: "file:///private/tmp/pm_crash_log.txt")!
|
|
NSWorkspace.shared.open(fileUrl)
|
|
})
|
|
.withSecondary(text: "crash_reporter.do_not_send".localized, action: { alert in
|
|
alert.close(with: .abort)
|
|
})
|
|
.withPrimary(text: "crash_reporter.send_report".localized, action: { alert in
|
|
alert.close(with: .OK)
|
|
}).runModal(urgency: .urgentRequestAttention)
|
|
|
|
// Check the outcome of what the user chose
|
|
if response == .abort {
|
|
Log.warn("[CrashReporter] The user has chosen not to send the report.")
|
|
crashReporter.purgePendingCrashReport()
|
|
}
|
|
if response == .OK {
|
|
submitCrashReportToApi(text)
|
|
crashReporter.purgePendingCrashReport()
|
|
}
|
|
} else {
|
|
Log.err("[CrashReporter] Could not convert report to text.")
|
|
crashReporter.purgePendingCrashReport()
|
|
}
|
|
} catch let error {
|
|
Log.err("[CrashReporter] Failed to load and parse with error: \(error)")
|
|
crashReporter.purgePendingCrashReport()
|
|
}
|
|
}
|
|
|
|
/**
|
|
Submits the crash report to the API. Does this with high priority on the main thread
|
|
and we wait for completion (w/ a DispatchSemaphore) before continuing boot.
|
|
*/
|
|
private static func submitCrashReportToApi(_ text: String) {
|
|
let timeout = TimeInterval.seconds(10)
|
|
|
|
var request = URLRequest(url: Constants.Urls.CrashReportingEndpoint)
|
|
request.httpMethod = "POST"
|
|
request.setValue("text/crash", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("phpmon-crashrep/1.0", forHTTPHeaderField: "User-Agent")
|
|
request.setValue(App.shared.getApiId(), forHTTPHeaderField: "X-phpmon-session-uuid")
|
|
request.httpBody = text.data(using: .utf8)
|
|
request.timeoutInterval = timeout
|
|
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
|
|
let task = URLSession.shared.dataTask(with: request) { _, response, error in
|
|
defer { semaphore.signal() }
|
|
|
|
if let error = error {
|
|
Log.err("[CrashReporter] Failed to send crash report: \(error.localizedDescription)")
|
|
return
|
|
}
|
|
|
|
if let httpResponse = response as? HTTPURLResponse {
|
|
switch httpResponse.statusCode {
|
|
case 200...299:
|
|
Log.info("[CrashReporter] Crash report sent successfully!")
|
|
case 400...499:
|
|
Log.err("[CrashReporter] Client error when sending crash report: \(httpResponse.statusCode)")
|
|
case 500...599:
|
|
Log.err("[CrashReporter] Server error when sending crash report: \(httpResponse.statusCode)")
|
|
default:
|
|
Log.err("[CrashReporter] Unexpected response code: \(httpResponse.statusCode)")
|
|
}
|
|
}
|
|
}
|
|
|
|
task.resume()
|
|
_ = semaphore.wait(timeout: .now() + timeout)
|
|
}
|
|
|
|
/**
|
|
Determines whether a debugger is attached.
|
|
*/
|
|
private static func isDebuggerAttached() -> Bool {
|
|
var info = kinfo_proc()
|
|
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
|
|
var size = MemoryLayout<kinfo_proc>.stride
|
|
|
|
let result = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0)
|
|
|
|
return result == 0 && (info.kp_proc.p_flag & P_TRACED) != 0
|
|
}
|
|
}
|