diff --git a/Logger.swift b/Logger.swift index d21a9b6..d197e37 100644 --- a/Logger.swift +++ b/Logger.swift @@ -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) } - diff --git a/NearbyShare/OutboundNearbyConnection.swift b/NearbyShare/OutboundNearbyConnection.swift index a0a25f9..db4df49 100644 --- a/NearbyShare/OutboundNearbyConnection.swift +++ b/NearbyShare/OutboundNearbyConnection.swift @@ -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 { diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index 99ac0b3..62923d5 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -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; diff --git a/QuickDrop/QuickDrop.entitlements b/QuickDrop/QuickDrop.entitlements index 02ce7ec..15a0582 100644 --- a/QuickDrop/QuickDrop.entitlements +++ b/QuickDrop/QuickDrop.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.com.leonboettger.neardrop + com.apple.security.files.downloads.read-write com.apple.security.files.user-selected.read-write diff --git a/QuickDrop/TutorialView.swift b/QuickDrop/TutorialView.swift index c781819..bf2ea05 100644 --- a/QuickDrop/TutorialView.swift +++ b/QuickDrop/TutorialView.swift @@ -98,7 +98,6 @@ struct LargeAppIconView: View { let title: String let bottomView: () -> Content - @State var taps = 0 var body: some View { ScrollView { @@ -108,18 +107,6 @@ struct LargeAppIconView: 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)" } diff --git a/QuickDrop/WelcomeScreen.swift b/QuickDrop/WelcomeScreen.swift index 7950c0f..dddab13 100644 --- a/QuickDrop/WelcomeScreen.swift +++ b/QuickDrop/WelcomeScreen.swift @@ -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) diff --git a/ShareExtension/ShareExtension.entitlements b/ShareExtension/ShareExtension.entitlements index 40b639e..3b6a8fd 100644 --- a/ShareExtension/ShareExtension.entitlements +++ b/ShareExtension/ShareExtension.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.com.leonboettger.neardrop + com.apple.security.files.user-selected.read-only com.apple.security.network.client