mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Unify main app + app extension logging
This commit is contained in:
1 parent
73e86ff65f
commit
23705c5e26
7 files changed
+85
-98
No files matched your search
+54
-74
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user