1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-03-26 06:00:07 +01:00
Files
app/phpmon/Domain/App/CrashReporter.swift
Nico Verbruggen c394248dc9 🔧 Renamed field to X-phpmon-session-uuid
I've also documented how this UUID is used to the best of my ability.
2025-12-01 23:40:40 +01:00

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
}
}