Unify main app + app extension logging

This commit is contained in:
Leon Böttger committed 2025-07-23 14:21:58 +02:00
1 parent 73e86ff65f
commit 23705c5e26
7 files changed
+85 -98

No files matched your search

+54 -74
View File
@@ -8,7 +8,7 @@
import Foundation
import os
fileprivate let loggerQueue = DispatchQueue(label: "loggerQueue")
fileprivate let loggerQueue = DispatchQueue(label: "loggerQueue", qos: .utility)
/// Manages logging functionality
final class LogManager {
@@ -16,132 +16,112 @@ final class LogManager {
/// The URL of the log file
let logFileURL: URL?
/// The private initializer
/// Shared instance
public static let sharedInstance = LogManager()
@Published var loggingEnabled = true
/// Private initializer
private init() {
// Use the App Group container
let directory = getAppGroupDirectory()
/// Get documents directory
let directory = getDocumentsDirectory()
/// Create file name from date
// Create file name from date
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
let dateString = formatter.string(from: Date())
let fileName = "\(dateString).log"
/// Create file URL from file name
self.logFileURL = directory.appendingPathComponent(fileName)
// Full path in app group container
self.logFileURL = directory?.appendingPathComponent(fileName)
}
/// The shared instance.
public static let sharedInstance = LogManager()
@Published var loggingEnabled = true
/// Writes a specified string to the log file.
func writeToFile(string: String) {
guard let logFileURL = logFileURL else { return }
/// Make sure the URL is not nil
guard let logFileURL = logFileURL else {
return
}
/// Get current timestamp
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
let timestamp = formatter.string(from: Date())
/// Create data from string and timestamp
guard let data = (timestamp + " - " + string + "\n").data(using: .utf8) else { return }
/// a log file exists already
if FileManager.default.fileExists(atPath: logFileURL.path) {
loggerQueue.async {
let fileManager = FileManager.default
/// we try to get a handle to write to the log file
if let file = try? FileHandle(forWritingTo: logFileURL) {
/// go to end of file
file.seekToEndOfFile()
/// write string to end
file.write(data)
///close file
file.closeFile()
if !fileManager.fileExists(atPath: logFileURL.path) {
// Clean up older logs
self.deleteLogs()
// Create file atomically
try? data.write(to: logFileURL, options: .atomicWrite)
return
}
} else {
/// Clean up older logs
deleteLogs()
/// create new log file
try? data.write(to: logFileURL, options: .atomicWrite)
}
}
func getLogString() -> String {
if let logFileURL = logFileURL {
let fileURL = URL(fileURLWithPath: logFileURL.path)
// Atomic append with file locking using FileHandle
do {
let content = try String(contentsOf: fileURL, encoding: .utf8)
return String(content)
let fileHandle = try FileHandle(forWritingTo: logFileURL)
defer { try? fileHandle.close() }
// Locking is handled by the serial dispatch queue
try fileHandle.seekToEnd()
try fileHandle.write(contentsOf: data)
} catch {
log("[LUI] Error reading file: \(error.localizedDescription)")
print("[Logger] Failed to write to log file: \(error.localizedDescription)")
}
}
return ""
}
/// Deletes all log files.
/// Gets the contents of the log file.
func getLogString() -> String {
guard let logFileURL = logFileURL else { return "" }
do {
return try String(contentsOf: logFileURL, encoding: .utf8)
} catch {
log("[LUI] Error reading file: \(error.localizedDescription)")
return ""
}
}
/// Deletes all log files in the shared container.
func deleteLogs() {
log("[LUI] Deleting all logs...")
guard let directory = getAppGroupDirectory() else { return }
let fileManager = FileManager.default
let documentsDirectory = getDocumentsDirectory()
guard let allFiles = try? fileManager.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil) else {
guard let allFiles = try? fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else {
return
}
let logFiles = allFiles.filter { $0.pathExtension.elementsEqual("log") }
let logFiles = allFiles.filter { $0.pathExtension == "log" }
for file in logFiles {
do {
log("[LUI] Removing \(file.description)")
log("[LUI] Removing \(file.lastPathComponent)")
try fileManager.removeItem(at: file)
} catch {
print(error.localizedDescription)
print("[Logger] Failed to delete file: \(error.localizedDescription)")
}
}
}
}
/// Logs a specified string.
/// Logs a specified string
public func log(_ text: String) {
let logger = LogManager.sharedInstance
if logger.loggingEnabled {
loggerQueue.async {
print(text)
// Weird bug?
#if !os(tvOS)
logger.writeToFile(string: text)
let isAppExtension = Bundle.main.bundlePath.hasSuffix(".appex")
let prefix = isAppExtension ? "[EXT]" : "[APP]"
logger.writeToFile(string: "\(prefix) \(text)")
#endif
}
}
}
/// Returns the URL of the documents directory.
func getDocumentsDirectory() -> URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
/// Returns the shared App Group directory
fileprivate func getAppGroupDirectory() -> URL? {
let groupIdentifier = "group.com.leonboettger.neardrop"
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier)
}
+1 -1
View File
@@ -420,8 +420,8 @@ class OutboundNearbyConnection: NearbyConnection {
self.protocolError()
}
})
log("sent file chunk, current transfer: \(String(describing: currentTransfer))")
totalBytesSent += Int64(fileBuffer.count)
log("sent file chunk \(totalBytesSent)")
delegate?.outboundConnection(connection: self, transferProgress: Double(totalBytesSent) / Double(totalBytesToSend))
if currentTransfer!.currentOffset == currentTransfer!.totalBytes {
+4
View File
@@ -728,6 +728,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.leonboettger.neardrop.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@@ -760,6 +761,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.leonboettger.neardrop.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
@@ -923,6 +925,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.leonboettger.neardrop;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -962,6 +965,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.leonboettger.neardrop;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
REGISTER_APP_GROUPS = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_VERSION = 5.0;
+4
View File
@@ -4,6 +4,10 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.leonboettger.neardrop</string>
</array>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
+17 -16
View File
@@ -98,7 +98,6 @@ struct LargeAppIconView<Content: View>: View {
let title: String
let bottomView: () -> Content
@State var taps = 0
var body: some View {
ScrollView {
@@ -108,18 +107,6 @@ struct LargeAppIconView<Content: View>: View {
.resizable()
.frame(width: 150, height: 150)
.padding(.top, 50)
.onTapGesture {
taps += 1
log("Clicked \(taps) times")
if taps == 5 {
sendLoggingString()
log("Copied log to clipboard")
taps = 0
}
}
Text(title.localized())
.font(.largeTitle)
@@ -139,12 +126,26 @@ func sendLoggingString() {
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
sendEmailWithAttachment(fileURL: url, recipients: ["quickdrop@leonboettger.com"], subject: "QuickDrop \(appVersion) Log")
sendEmailWithAttachment(fileURL: url, recipients: ["quickdrop@leonboettger.com"], subject: "QuickDrop \(appVersion) - \(getDeviceAndSystem())")
}
}
fileprivate func getDeviceAndSystem() -> String {
// Get Mac model identifier
var size: Int = 0
sysctlbyname("hw.model", nil, &size, nil, 0)
let logString = LogManager.sharedInstance.getLogString()
var modelBuffer = [CChar](repeating: 0, count: size)
sysctlbyname("hw.model", &modelBuffer, &size, nil, 0)
copyToClipboard(logString)
let model = String(cString: modelBuffer)
// Get macOS version
let version = ProcessInfo.processInfo.operatingSystemVersion
let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
return "\(model) - \(versionString)"
}
+1 -7
View File
@@ -33,7 +33,7 @@ struct WelcomeScreen: View {
Divider()
ExternalLinkLabel(label: "GetSupport", icon: "questionmark.circle") {
getSupport()
sendLoggingString()
}
ExternalLinkLabel(label: "PrivacyPolicy", icon: "hand.raised") {
@@ -85,12 +85,6 @@ struct WelcomeScreen: View {
.frame(width: 1000, height: 600)
}
func getSupport() {
if let url = URL(string: "mailto:quickdrop@leonboettger.com?subject=QuickDrop") {
NSWorkspace.shared.open(url)
}
}
func openPrivacyPolicy() {
if let url = URL(string: "https://leonboettger.com/quickdrop-privacy") {
NSWorkspace.shared.open(url)
@@ -4,6 +4,10 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.leonboettger.neardrop</string>
</array>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>