From 16acce08b9039aaecdb8f3bf372eea3b04fbc0d9 Mon Sep 17 00:00:00 2001 From: Erik Verbruggen Date: Wed, 10 Nov 2021 18:05:55 +0100 Subject: macOS: Use local socket to communicate with finder extension https://github.com/owncloud/client/issues/6343 --- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../OwnCloudFinderSync/FinderSyncExt/FinderSync.h | 20 +- .../OwnCloudFinderSync/FinderSyncExt/FinderSync.m | 311 +++++++++++---------- .../FinderSyncExt/FinderSyncExt-Bridging-Header.h | 5 + .../FinderSyncExt/LineProcessorV1.swift | 67 +++++ .../FinderSyncExt/LocalSocketClient.swift | 289 +++++++++++++++++++ .../OwnCloudFinderSync/FinderSyncExt/SyncClient.h | 27 ++ .../FinderSyncExt/SyncClientProxy.h | 45 --- .../FinderSyncExt/SyncClientProxy.m | 161 ----------- .../OwnCloudFinderSync.xcodeproj/project.pbxproj | 36 ++- src/gui/guiutility_mac.mm | 10 +- src/gui/socketapi/CMakeLists.txt | 4 - src/gui/socketapi/socketapi.cpp | 37 +-- src/gui/socketapi/socketapi.h | 2 +- src/gui/socketapi/socketapisocket_mac.h | 69 ----- src/gui/socketapi/socketapisocket_mac.mm | 273 ------------------ 16 files changed, 627 insertions(+), 737 deletions(-) create mode 100644 shell_integration/MacOSX/OwnCloud.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSyncExt-Bridging-Header.h create mode 100644 shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LineProcessorV1.swift create mode 100644 shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LocalSocketClient.swift create mode 100644 shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/SyncClient.h delete mode 100644 shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/SyncClientProxy.h delete mode 100644 shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/SyncClientProxy.m delete mode 100644 src/gui/socketapi/socketapisocket_mac.h delete mode 100644 src/gui/socketapi/socketapisocket_mac.mm diff --git a/shell_integration/MacOSX/OwnCloud.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/shell_integration/MacOSX/OwnCloud.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/shell_integration/MacOSX/OwnCloud.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSync.h b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSync.h index 803c5270f..27d7a5a02 100644 --- a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSync.h +++ b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSync.h @@ -15,15 +15,19 @@ #import #import -#import "SyncClientProxy.h" -@interface FinderSync : FIFinderSync -{ - SyncClientProxy *_syncClientProxy; - NSMutableSet *_registeredDirectories; - NSString *_shareMenuTitle; - NSMutableDictionary *_strings; - NSMutableArray *_menuItems; +#import "FinderSyncExt-Swift.h" +#import "SyncClient.h" + +@interface FinderSync : FIFinderSync { + NSMutableSet *_registeredDirectories; + NSString *_shareMenuTitle; + NSMutableDictionary *_strings; + NSMutableArray *_menuItems; + NSCondition *_menuIsComplete; } +@property LineProcessorV1 *v1LineProcessor; +@property LocalSocketClient *localSocketClient; + @end diff --git a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSync.m b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSync.m index a0f791882..0f95465f2 100644 --- a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSync.m +++ b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSync.m @@ -20,190 +20,219 @@ - (instancetype)init { - self = [super init]; - - FIFinderSyncController *syncController = [FIFinderSyncController defaultController]; - NSBundle *extBundle = [NSBundle bundleForClass:[self class]]; - // This was added to the bundle's Info.plist to get it from the build system - NSString *socketApiPrefix = [extBundle objectForInfoDictionaryKey:@"SocketApiPrefix"]; - - NSImage *ok = [extBundle imageForResource:@"ok.icns"]; - NSImage *ok_swm = [extBundle imageForResource:@"ok_swm.icns"]; - NSImage *sync = [extBundle imageForResource:@"sync.icns"]; - NSImage *warning = [extBundle imageForResource:@"warning.icns"]; - NSImage *error = [extBundle imageForResource:@"error.icns"]; - - [syncController setBadgeImage:ok label:@"Up to date" forBadgeIdentifier:@"OK"]; - [syncController setBadgeImage:sync label:@"Synchronizing" forBadgeIdentifier:@"SYNC"]; - [syncController setBadgeImage:sync label:@"Synchronizing" forBadgeIdentifier:@"NEW"]; - [syncController setBadgeImage:warning label:@"Ignored" forBadgeIdentifier:@"IGNORE"]; - [syncController setBadgeImage:error label:@"Error" forBadgeIdentifier:@"ERROR"]; - [syncController setBadgeImage:ok_swm label:@"Shared" forBadgeIdentifier:@"OK+SWM"]; - [syncController setBadgeImage:sync label:@"Synchronizing" forBadgeIdentifier:@"SYNC+SWM"]; - [syncController setBadgeImage:sync label:@"Synchronizing" forBadgeIdentifier:@"NEW+SWM"]; - [syncController setBadgeImage:warning label:@"Ignored" forBadgeIdentifier:@"IGNORE+SWM"]; - [syncController setBadgeImage:error label:@"Error" forBadgeIdentifier:@"ERROR+SWM"]; - - // The Mach port name needs to: - // - Be prefixed with the code signing Team ID - // - Then infixed with the sandbox App Group - // - The App Group itself must be a prefix of (or equal to) the application bundle identifier - // We end up in the official signed client with: 9B5WD74GWJ.com.owncloud.desktopclient.socketApi - // With ad-hoc signing (the '-' signing identity) we must drop the Team ID. - // When the code isn't sandboxed (e.g. the OC client or the legacy overlay icon extension) - // the OS doesn't seem to put any restriction on the port name, so we just follow what - // the sandboxed App Extension needs. - // https://developer.apple.com/library/mac/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW24 - NSString *serverName = [socketApiPrefix stringByAppendingString:@".socketApi"]; - //NSLog(@"FinderSync serverName %@", serverName); - - _syncClientProxy = [[SyncClientProxy alloc] initWithDelegate:self serverName:serverName]; - _registeredDirectories = [[NSMutableSet alloc] init]; - _strings = [[NSMutableDictionary alloc] init]; - - [_syncClientProxy start]; - return self; + self = [super init]; + + FIFinderSyncController *syncController = [FIFinderSyncController defaultController]; + NSBundle *extBundle = [NSBundle bundleForClass:[self class]]; + // This was added to the bundle's Info.plist to get it from the build system + NSString *socketApiPrefix = [extBundle objectForInfoDictionaryKey:@"SocketApiPrefix"]; + + NSImage *ok = [extBundle imageForResource:@"ok.icns"]; + NSImage *ok_swm = [extBundle imageForResource:@"ok_swm.icns"]; + NSImage *sync = [extBundle imageForResource:@"sync.icns"]; + NSImage *warning = [extBundle imageForResource:@"warning.icns"]; + NSImage *error = [extBundle imageForResource:@"error.icns"]; + + [syncController setBadgeImage:ok label:@"Up to date" forBadgeIdentifier:@"OK"]; + [syncController setBadgeImage:sync label:@"Synchronizing" forBadgeIdentifier:@"SYNC"]; + [syncController setBadgeImage:sync label:@"Synchronizing" forBadgeIdentifier:@"NEW"]; + [syncController setBadgeImage:warning label:@"Ignored" forBadgeIdentifier:@"IGNORE"]; + [syncController setBadgeImage:error label:@"Error" forBadgeIdentifier:@"ERROR"]; + [syncController setBadgeImage:ok_swm label:@"Shared" forBadgeIdentifier:@"OK+SWM"]; + [syncController setBadgeImage:sync label:@"Synchronizing" forBadgeIdentifier:@"SYNC+SWM"]; + [syncController setBadgeImage:sync label:@"Synchronizing" forBadgeIdentifier:@"NEW+SWM"]; + [syncController setBadgeImage:warning label:@"Ignored" forBadgeIdentifier:@"IGNORE+SWM"]; + [syncController setBadgeImage:error label:@"Error" forBadgeIdentifier:@"ERROR+SWM"]; + + // The Mach port name needs to: + // - Be prefixed with the code signing Team ID + // - Then infixed with the sandbox App Group + // - The App Group itself must be a prefix of (or equal to) the application bundle identifier + // We end up in the official signed client with: 9B5WD74GWJ.com.owncloud.desktopclient.socketApi + // With ad-hoc signing (the '-' signing identity) we must drop the Team ID. + // When the code isn't sandboxed (e.g. the OC client or the legacy overlay icon extension) + // the OS doesn't seem to put any restriction on the port name, so we just follow what + // the sandboxed App Extension needs. + // https://developer.apple.com/library/mac/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW24 + + NSURL *container = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:socketApiPrefix]; + NSURL *socketPath = [container URLByAppendingPathComponent:@"GUI.socket" isDirectory:false]; + + if (socketPath.path) { + self.v1LineProcessor = [[LineProcessorV1 alloc] initWithDelegate:self]; + self.localSocketClient = [[LocalSocketClient alloc] initWithSocketPath:socketPath.path + lineProcessor:self.v1LineProcessor]; + [self.localSocketClient start]; + } else { + self.localSocketClient = nil; + } + _registeredDirectories = [[NSMutableSet alloc] init]; + _strings = [[NSMutableDictionary alloc] init]; + _menuIsComplete = [[NSCondition alloc] init]; + + return self; } #pragma mark - Primary Finder Sync protocol methods - (void)requestBadgeIdentifierForURL:(NSURL *)url { - BOOL isDir; - if ([[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory: &isDir] == NO) { - NSLog(@"ERROR: Could not determine file type of %@", [url path]); - isDir = NO; - } - - NSString* normalizedPath = [[url path] decomposedStringWithCanonicalMapping]; - [_syncClientProxy askForIcon:normalizedPath isDirectory:isDir]; + BOOL isDir; + if ([[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory:&isDir] == NO) { + NSLog(@"ERROR: Could not determine file type of %@", [url path]); + isDir = NO; + } + + NSString *normalizedPath = [[url path] decomposedStringWithCanonicalMapping]; + [self.localSocketClient askForIcon:normalizedPath isDirectory:isDir]; } #pragma mark - Menu and toolbar item support -- (NSString*) selectedPathsSeparatedByRecordSeparator +- (NSString *)selectedPathsSeparatedByRecordSeparator { - FIFinderSyncController *syncController = [FIFinderSyncController defaultController]; - NSMutableString *string = [[NSMutableString alloc] init]; - [syncController.selectedItemURLs enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) { - if (string.length > 0) { - [string appendString:@"\x1e"]; // record separator - } - NSString* normalizedPath = [[obj path] decomposedStringWithCanonicalMapping]; - [string appendString:normalizedPath]; - }]; - return string; + FIFinderSyncController *syncController = [FIFinderSyncController defaultController]; + NSMutableString *string = [[NSMutableString alloc] init]; + [syncController.selectedItemURLs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + if (string.length > 0) { + [string appendString:@"\x1e"]; // record separator + } + NSString *normalizedPath = [[obj path] decomposedStringWithCanonicalMapping]; + [string appendString:normalizedPath]; + }]; + return string; } - (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu { - FIFinderSyncController *syncController = [FIFinderSyncController defaultController]; - NSMutableSet *rootPaths = [[NSMutableSet alloc] init]; - [syncController.directoryURLs enumerateObjectsUsingBlock: ^(id obj, BOOL *stop) { - [rootPaths addObject:[obj path]]; - }]; - - // The server doesn't support sharing a root directory so do not show the option in this case. - // It is still possible to get a problematic sharing by selecting both the root and a child, - // but this is so complicated to do and meaningless that it's not worth putting this check - // also in shareMenuAction. - __block BOOL onlyRootsSelected = YES; - [syncController.selectedItemURLs enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) { - if (![rootPaths member:[obj path]]) { - onlyRootsSelected = NO; - *stop = YES; - } - }]; - - NSString *paths = [self selectedPathsSeparatedByRecordSeparator]; - // calling this IPC calls us back from client with several MENU_ITEM entries and then our askOnSocket returns again - [_syncClientProxy askOnSocket:paths query:@"GET_MENU_ITEMS"]; - - id contextMenuTitle = [_strings objectForKey:@"CONTEXT_MENU_TITLE"]; - if (contextMenuTitle && !onlyRootsSelected) { - NSMenu *menu = [[NSMenu alloc] initWithTitle:@""]; - NSMenu *subMenu = [[NSMenu alloc] initWithTitle:@""]; - NSMenuItem *subMenuItem = [menu addItemWithTitle:contextMenuTitle action:nil keyEquivalent:@""]; - subMenuItem.submenu = subMenu; - subMenuItem.image = [[NSBundle mainBundle] imageForResource:@"app.icns"]; - - // There is an annoying bug in macOS (at least 10.13.3), it does not use/copy over the representedObject of a menu item - // So we have to use tag instead. - int idx = 0; - for (NSArray* item in _menuItems) { - NSMenuItem *actionItem = [subMenu addItemWithTitle:[item valueForKey:@"text"] - action:@selector(subMenuActionClicked:) - keyEquivalent:@""]; - [actionItem setTag:idx]; - [actionItem setTarget:self]; - NSString *flags = [item valueForKey:@"flags"]; // e.g. "d" - if ([flags rangeOfString:@"d"].location != NSNotFound) { - [actionItem setEnabled:false]; - } - idx++; - } - return menu; - } - return nil; + if (!self.localSocketClient.isConnected) { + return nil; + } + + FIFinderSyncController *syncController = [FIFinderSyncController defaultController]; + NSMutableSet *rootPaths = [[NSMutableSet alloc] init]; + [syncController.directoryURLs enumerateObjectsUsingBlock:^(id obj, BOOL *stop) { + [rootPaths addObject:[obj path]]; + }]; + + // The server doesn't support sharing a root directory so do not show the option in this case. + // It is still possible to get a problematic sharing by selecting both the root and a child, + // but this is so complicated to do and meaningless that it's not worth putting this check + // also in shareMenuAction. + __block BOOL onlyRootsSelected = YES; + [syncController.selectedItemURLs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + if (![rootPaths member:[obj path]]) { + onlyRootsSelected = NO; + *stop = YES; + } + }]; + + NSString *paths = [self selectedPathsSeparatedByRecordSeparator]; + [self.localSocketClient askOnSocket:paths query:@"GET_MENU_ITEMS"]; + + // The LocalSocketClient uses asynchronous communication, so we have to wait here until the menu items have + // delivered by another thread. + [self waitForMenuToArrive]; + + id contextMenuTitle = [_strings objectForKey:@"CONTEXT_MENU_TITLE"]; + if (contextMenuTitle && !onlyRootsSelected) { + NSMenu *menu = [[NSMenu alloc] initWithTitle:@""]; + NSMenu *subMenu = [[NSMenu alloc] initWithTitle:@""]; + NSMenuItem *subMenuItem = [menu addItemWithTitle:contextMenuTitle action:nil keyEquivalent:@""]; + subMenuItem.submenu = subMenu; + subMenuItem.image = [[NSBundle mainBundle] imageForResource:@"app.icns"]; + + // There is an annoying bug in macOS (at least 10.13.3), it does not use/copy over the representedObject of a menu item + // So we have to use tag instead. + int idx = 0; + for (NSArray *item in _menuItems) { + NSMenuItem *actionItem = [subMenu addItemWithTitle:[item valueForKey:@"text"] + action:@selector(subMenuActionClicked:) + keyEquivalent:@""]; + [actionItem setTag:idx]; + [actionItem setTarget:self]; + NSString *flags = [item valueForKey:@"flags"]; // e.g. "d" + if ([flags rangeOfString:@"d"].location != NSNotFound) { + [actionItem setEnabled:false]; + } + idx++; + } + return menu; + } + return nil; } -- (void)subMenuActionClicked:(id)sender { - long idx = [(NSMenuItem*)sender tag]; - NSString *command = [[_menuItems objectAtIndex:idx] valueForKey:@"command"]; - NSString *paths = [self selectedPathsSeparatedByRecordSeparator]; - [_syncClientProxy askOnSocket:paths query:command]; +- (void)waitForMenuToArrive +{ + [self->_menuIsComplete lock]; + [self->_menuIsComplete wait]; + [self->_menuIsComplete unlock]; +} + +- (void)subMenuActionClicked:(id)sender +{ + long idx = [(NSMenuItem *)sender tag]; + NSString *command = [[_menuItems objectAtIndex:idx] valueForKey:@"command"]; + NSString *paths = [self selectedPathsSeparatedByRecordSeparator]; + [self.localSocketClient askOnSocket:paths query:command]; } #pragma mark - SyncClientProxyDelegate implementation -- (void)setResultForPath:(NSString*)path result:(NSString*)result +- (void)setResultForPath:(NSString *)path result:(NSString *)result { - NSString *normalizedPath = [path decomposedStringWithCanonicalMapping]; - [[FIFinderSyncController defaultController] setBadgeIdentifier:result forURL:[NSURL fileURLWithPath:normalizedPath]]; + NSString *normalizedPath = [path decomposedStringWithCanonicalMapping]; + [[FIFinderSyncController defaultController] setBadgeIdentifier:result forURL:[NSURL fileURLWithPath:normalizedPath]]; } -- (void)reFetchFileNameCacheForPath:(NSString*)path +- (void)reFetchFileNameCacheForPath:(NSString *)path { } -- (void)registerPath:(NSString*)path +- (void)registerPath:(NSString *)path { - assert(_registeredDirectories); - [_registeredDirectories addObject:[NSURL fileURLWithPath:path]]; - [FIFinderSyncController defaultController].directoryURLs = _registeredDirectories; + assert(_registeredDirectories); + [_registeredDirectories addObject:[NSURL fileURLWithPath:path]]; + [FIFinderSyncController defaultController].directoryURLs = _registeredDirectories; } -- (void)unregisterPath:(NSString*)path +- (void)unregisterPath:(NSString *)path { - [_registeredDirectories removeObject:[NSURL fileURLWithPath:path]]; - [FIFinderSyncController defaultController].directoryURLs = _registeredDirectories; + [_registeredDirectories removeObject:[NSURL fileURLWithPath:path]]; + [FIFinderSyncController defaultController].directoryURLs = _registeredDirectories; } -- (void)setString:(NSString*)key value:(NSString*)value +- (void)setString:(NSString *)key value:(NSString *)value { - [_strings setObject:value forKey:key]; + [_strings setObject:value forKey:key]; } - (void)resetMenuItems { - _menuItems = [[NSMutableArray alloc] init]; + _menuItems = [[NSMutableArray alloc] init]; +} + +- (void)addMenuItem:(NSDictionary *)item +{ + [_menuItems addObject:item]; } -- (void)addMenuItem:(NSDictionary *)item { - [_menuItems addObject:item]; + +- (void)menuHasCompleted +{ + [self->_menuIsComplete signal]; } - (void)connectionDidDie { - [_strings removeAllObjects]; - [_registeredDirectories removeAllObjects]; - // For some reason the FIFinderSync cache doesn't seem to be cleared for the root item when - // we reset the directoryURLs (seen on macOS 10.12 at least). - // First setting it to the FS root and then setting it to nil seems to work around the issue. - [FIFinderSyncController defaultController].directoryURLs = [NSSet setWithObject:[NSURL fileURLWithPath:@"/"]]; - // This will tell Finder that this extension isn't attached to any directory - // until we can reconnect to the sync client. - [FIFinderSyncController defaultController].directoryURLs = nil; + [_strings removeAllObjects]; + [_registeredDirectories removeAllObjects]; + // For some reason the FIFinderSync cache doesn't seem to be cleared for the root item when + // we reset the directoryURLs (seen on macOS 10.12 at least). + // First setting it to the FS root and then setting it to nil seems to work around the issue. + [FIFinderSyncController defaultController].directoryURLs = [NSSet setWithObject:[NSURL fileURLWithPath:@"/"]]; + // This will tell Finder that this extension isn't attached to any directory + // until we can reconnect to the sync client. + [FIFinderSyncController defaultController].directoryURLs = nil; } @end - diff --git a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSyncExt-Bridging-Header.h b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSyncExt-Bridging-Header.h new file mode 100644 index 000000000..a363d7439 --- /dev/null +++ b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/FinderSyncExt-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "SyncClient.h" diff --git a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LineProcessorV1.swift b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LineProcessorV1.swift new file mode 100644 index 000000000..a554a0f0a --- /dev/null +++ b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LineProcessorV1.swift @@ -0,0 +1,67 @@ +// +// LineProcessorV1.swift +// FinderSyncExt +// +// Created by Erik Verbruggen on 06-11-21. +// + +import Foundation +import OSLog + +class LineProcessorV1: NSObject, LineProcessor { + let delegate: SyncClientDelegate + + @objc init(withDelegate delegate: SyncClientDelegate) { + self.delegate = delegate + } + + private func log(_ str: String, type logType: OSLogType) { + // We cannot use OSLog's Logger class, because a lot of methods are only available in macOS 11.0 or higher. + os_log("%@", type: logType, str) + } + + /// Processes a line, where the trailing \n has already been stripped + func process(line: String) { + + self.log("Processing line '\(line)'", type: .debug) + let chunks = line.components(separatedBy: ":") + let command = chunks[0] + + switch command { + case "STATUS": + let result = chunks[1] + let path = chunks.suffix(from: 2).joined(separator: ":") + DispatchQueue.main.async { self.delegate.setResultForPath(path, result: result) } + case "UPDATE_VIEW": + let path = chunks[1] + DispatchQueue.main.async { self.delegate.reFetchFileNameCache(forPath: path) } + case "REGISTER_PATH": + let path = chunks[1] + DispatchQueue.main.async { self.delegate.registerPath(path) } + case "UNREGISTER_PATH": + let path = chunks[1] + DispatchQueue.main.async { self.delegate.unregisterPath(path) } + case "GET_STRINGS": + // BEGIN and END messages, do nothing. + break + case "STRING": + DispatchQueue.main.async { self.delegate.setString(chunks[1], value: chunks[2]) } + case "GET_MENU_ITEMS": + if chunks[1] == "BEGIN" { + DispatchQueue.main.async { self.delegate.resetMenuItems() } + } else { + // Do NOT run this on the main queue! It might be blocked waiting for the menu to be completed. + delegate.menuHasCompleted() + } + case "MENU_ITEM": + let item = [ + "command" : chunks[1], + "flags" : chunks[2], + "text" : chunks[3] + ] + DispatchQueue.main.async { self.delegate.addMenuItem(item) } + default: + self.log("Unknown command '\(command)'", type: .error) + } + } +} diff --git a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LocalSocketClient.swift b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LocalSocketClient.swift new file mode 100644 index 000000000..c0f2a3acd --- /dev/null +++ b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LocalSocketClient.swift @@ -0,0 +1,289 @@ +// +// LocalSocketClient.swift +// FinderSyncExt +// +// Created by Erik Verbruggen on 04-11-21. +// + +import Foundation +import OSLog + +/// Process lines from the `LocalSocketClient`. +@objc protocol LineProcessor { + func process(line: String); +} + +/// Class handling the (asynchronous) communication with a server over a local (UNIX) socket. +/// +/// The implementation uses a `DispatchQueue` and `DispatchSource`s to handle asynchronous communication and thread +/// safety. All public/@objc function can be called from any thread/queue. The delegate that handles the +/// line-decoding is **not invoked on the UI thread**, but the (random) thread associated with the `DispatchQueue`! +/// If any UI work needs to be done, the class implementing the `LineProcessor` protocol should dispatch this work +/// on the main queue (so the UI thread) itself! +/// +/// Other than the `init(withSocketPath:, lineProcessor)` and the `start()` method, all work is done "on the dispatch +/// queue". The `localSocketQueue` is a serial dispatch queue (so a maximum of 1, and only 1, task is run at any +/// moment), which guarantees safe access to instance variables. Both `askOnSocket(_:, query:)` and +/// `askForIcon(_:, isDirectory:)` will internally dispatch the work on the `DispatchQueue`. +/// +/// Sending and receiving data to and from the socket, is handled by two `DispatchSource`s. These will run an event +/// handler when data can be read from resp. written to the socket. These handlers will also be run on the +/// `DispatchQueue`. +class LocalSocketClient: NSObject { + let socketPath: String + let lineProcessor: LineProcessor + + private var sock: Int32? + private var localSocketQueue = DispatchQueue.init(label: "localSocketQueue") + private var readSource: DispatchSourceRead? + private var writeSource: DispatchSourceWrite? + private var inBuffer = [UInt8]() + private var outBuffer = [UInt8]() + + @objc var isConnected: Bool { + get { + sock != nil + } + } + + @objc init(withSocketPath socketPath: String, lineProcessor: LineProcessor) { + self.socketPath = socketPath + self.lineProcessor = lineProcessor + + super.init() + + self.inBuffer.reserveCapacity(1000) + } + + private func log(_ str: String, type logType: OSLogType) { + if #available(macOSApplicationExtension 11.0, *) { + // NOTE: when support for 10.* is dropped, make an instance variable instead of instantiating the `Logger` + // object every time. + Logger().log(level: logType, "\(str, privacy: .public)") + } else { + os_log("%@", type: logType, str) + } + } + + // MARK: Socket management + @objc func start() { + guard !self.isConnected else { return } + + var sa_un = sockaddr_un() + + let socketPathByteCount = socketPath.utf8.count + 1; // add 1 for the NUL terminator char + let maxByteCount = MemoryLayout.size(ofValue: sa_un.sun_path) + guard socketPathByteCount < maxByteCount else { + log("Socket path '\(socketPath)' is too long: \(socketPathByteCount) is longer than \(maxByteCount)", + type: .error) + return + } + + log("Opening local socket...", type: .debug) + + self.sock = socket(AF_LOCAL, SOCK_STREAM, 0) + guard self.sock != -1 else { + self.log("Cannot open socket: \(self.strErr())", type: .error) + self.restart() + return + } + + log("Local socket openned, now connecting to '\(self.socketPath)' ...", type: .debug) + + sa_un.sun_family = UInt8(AF_LOCAL & 0xff) + + let pathBytes = socketPath.utf8 + [0] + pathBytes.withUnsafeBytes { srcBuffer in + withUnsafeMutableBytes(of: &sa_un.sun_path) { dstPtr in + dstPtr.copyMemory(from: srcBuffer) + } + } + + let connStatus = withUnsafePointer(to: &sa_un) { sa_unPtr in + // We are now allowed to mess with the raw pointer to `sa_un`, and cast it to a `sockaddr` pointer. + // This is basically a barrier before and after this closure, so that all writes have been done before by + // the compiler, and that subsequent reads do not read old values (because Swift can't see if `connect` + // messes with the memory for which it receives a raw pointer). + sa_unPtr.withMemoryRebound(to: sockaddr.self, capacity: 1) { saPtr in + connect(self.sock!, saPtr, socklen_t(MemoryLayout.size)) + } + } + + guard connStatus != -1 else { + self.log("Cannot connect to '\(self.socketPath): \(self.strErr())", type: .error) + self.restart() + return + } + + let flags = fcntl(self.sock!, F_GETFL, 0) + guard -1 != fcntl(self.sock!, F_SETFL, flags | O_NONBLOCK) else { + self.log("Cannot set socket to non-blocking mode: \(self.strErr())", type: .error) + self.restart() + return + } + + log("We have a connection! Starting dispatch sources...", type: .debug) + + self.readSource = DispatchSource.makeReadSource(fileDescriptor: self.sock!, queue: self.localSocketQueue) + self.readSource!.setEventHandler { self.readFromSocket() } + self.readSource!.setCancelHandler { + self.readSource = nil + self.closeConnection() + } + + self.writeSource = DispatchSource.makeWriteSource(fileDescriptor: self.sock!, queue: self.localSocketQueue) + self.writeSource!.setEventHandler { self.writeToSocket() } + self.writeSource!.setCancelHandler { + self.writeSource = nil + self.closeConnection() + } + // The writeSource dispatch queue starts suspended; we will resume it when we have data to send (and suspend it + // again when our send buffer is empty). + + log("Going live NOW.", type: .debug) + + self.readSource!.resume() + self.askOnSocket("", query: "GET_STRINGS") + } + + private func restart() { + self.closeConnection() + + DispatchQueue.main.async { + Timer.scheduledTimer(withTimeInterval: 5, repeats: false, block: { _ in + self.start() + }); + } + } + + private func closeConnection() { + self.readSource?.cancel() + self.writeSource?.cancel() + self.readSource = nil + self.writeSource = nil + self.inBuffer.removeAll() + self.outBuffer.removeAll() + if let sock = self.sock { + close(sock) + self.sock = nil + } + } + + private func strErr() -> String { + let err = errno // copy error code now, in case something else happens + return String(utf8String: strerror(err)) ?? "Unknown error code (\(err))" + } + + // MARK: Write data to socket + @objc func askOnSocket(_ path: String, query verb: String) { + let line = "\(verb):\(path)\n" + self.localSocketQueue.async { + guard self.isConnected else { + // socket was closed while work was still scheduled on the queue + return + } + + self.log("Sending line '\(line)", type: .debug) + + let writeSourceIsSuspended = self.outBuffer.isEmpty + let uint8Data: [UInt8] = line.utf8 + [] + self.outBuffer.append(contentsOf: uint8Data) + + // Weird stuff happens when you call resume when the DispatchSource is already resumed, so: if we did NOT + // have any data in our output buffer before queueing more data, it must be suspended. + if writeSourceIsSuspended { + self.writeSource?.resume() // now we will get notified when we can write to the socket. + } + } + } + + private func writeToSocket() { + guard self.isConnected else { + // socket was closed while work was still scheduled on the queue + return + } + + guard !self.outBuffer.isEmpty else { + // the buffer is empty, suspend you-can-write-data notifications + self.writeSource!.suspend() + return + } + + let totalAmountOfBytes = self.outBuffer.count + let bytesWritten = self.outBuffer.withUnsafeBytes { ptr in + write(self.sock!, ptr.baseAddress, totalAmountOfBytes) + } + if bytesWritten == 0 { + // we reached "end of file", which means that the socket was closed. So in that case: + self.restart() + } else if bytesWritten == -1 { + let err = errno // make a copy, because errno is often volatile + if err == EAGAIN || err == EWOULDBLOCK { + // no space free in the buffer on the OS side, we're done + } else { + self.log("Error writing to local socket: \(self.strErr())", type: .error) + self.restart() + } + } else if bytesWritten > 0 { + self.outBuffer.removeFirst(bytesWritten) + if self.outBuffer.isEmpty { + // the buffer is empty, suspend you-can-write-data notifications + self.writeSource!.suspend() + } + } + } + + @objc func askForIcon(_ path: String, isDirectory: Bool) { + let verb = isDirectory ? "RETRIEVE_FOLDER_STATUS" : "RETRIEVE_FILE_STATUS" + self.askOnSocket(path, query: verb) + } + + // MARK: Process data from socket + private func readFromSocket() { + guard self.isConnected else { + // socket was closed while work was still scheduled on the queue + return + } + + let bufferLength = self.inBuffer.capacity / 2 + var buffer = [UInt8].init(repeating: 0, count: bufferLength) + + while true { + let bytesRead = buffer.withUnsafeMutableBytes { ptr in + read(self.sock!, ptr.baseAddress, bufferLength) + } + if bytesRead == 0 { + // we reached "end of file", which means that the socket was closed. So in that case: + self.restart() + return + } else if bytesRead == -1 { + if errno == EAGAIN { + return // no bytes available, and no error, so we're done + } else { + self.log("Error reading from local socket: \(self.strErr())", type: .error) + self.closeConnection() + return // we've closed the connection, we're done + } + } else { + self.inBuffer.append(contentsOf: buffer[0.. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#import + +@protocol SyncClientDelegate +- (void)setResultForPath:(NSString *)path result:(NSString *)result; +- (void)reFetchFileNameCacheForPath:(NSString *)path; +- (void)registerPath:(NSString *)path; +- (void)unregisterPath:(NSString *)path; +- (void)setString:(NSString *)key value:(NSString *)value; +- (void)resetMenuItems; +- (void)addMenuItem:(NSDictionary *)item; +- (void)menuHasCompleted; +- (void)connectionDidDie; +@end diff --git a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/SyncClientProxy.h b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/SyncClientProxy.h deleted file mode 100644 index 1d0fd74b8..000000000 --- a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/SyncClientProxy.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) by Jocelyn Turcotte - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - */ - -#import - - -@protocol SyncClientProxyDelegate -- (void)setResultForPath:(NSString*)path result:(NSString*)result; -- (void)reFetchFileNameCacheForPath:(NSString*)path; -- (void)registerPath:(NSString*)path; -- (void)unregisterPath:(NSString*)path; -- (void)setString:(NSString*)key value:(NSString*)value; -- (void)resetMenuItems; -- (void)addMenuItem:(NSDictionary *)item; -- (void)connectionDidDie; -@end - -@protocol ChannelProtocol -- (void)sendMessage:(NSData*)msg; -@end - -@interface SyncClientProxy : NSObject -{ - NSString *_serverName; - NSDistantObject *_remoteEnd; -} - -@property (weak) id delegate; - -- (instancetype)initWithDelegate:(id)arg1 serverName:(NSString*)serverName; -- (void)start; -- (void)askOnSocket:(NSString*)path query:(NSString*)verb; -- (void)askForIcon:(NSString*)path isDirectory:(BOOL)isDir; -@end diff --git a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/SyncClientProxy.m b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/SyncClientProxy.m deleted file mode 100644 index 656f77003..000000000 --- a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/SyncClientProxy.m +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) by Jocelyn Turcotte - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - */ - -#import "SyncClientProxy.h" - -@protocol ServerProtocol -- (void)registerClient:(id)client; -@end - -@interface SyncClientProxy () -- (void)registerTransmitter:(id)tx; -@end - -@implementation SyncClientProxy - -- (instancetype)initWithDelegate:(id)arg1 serverName:(NSString*)serverName -{ - self = [super init]; - - self.delegate = arg1; - _serverName = serverName; - _remoteEnd = nil; - - return self; -} - -#pragma mark - Connection setup - -- (void)start -{ - if (_remoteEnd) - return; - - // Lookup the server connection - NSConnection *conn = [NSConnection connectionWithRegisteredName:_serverName host:nil]; - - if (!conn) { - // Could not connect to the sync client - [self scheduleRetry]; - return; - } - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(connectionDidDie:) - name:NSConnectionDidDieNotification - object:conn]; - - NSDistantObject *server = (NSDistantObject *)[conn rootProxy]; - assert(server); - - // This saves a few Mach messages, enable "Distributed Objects" in the scheme's Run diagnostics to watch - [server setProtocolForProxy:@protocol(ServerProtocol)]; - - // Send an object to the server to act as the channel rx, we'll receive the tx through registerTransmitter - [server registerClient:self]; -} - -- (void)registerTransmitter:(id)tx; -{ - // The server replied with the distant object that we will use for tx - _remoteEnd = (NSDistantObject *)tx; - [_remoteEnd setProtocolForProxy:@protocol(ChannelProtocol)]; - - // Everything is set up, start querying - [self askOnSocket:@"" query:@"GET_STRINGS"]; -} - -- (void)scheduleRetry -{ - [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(start) userInfo:nil repeats:NO]; -} - -- (void)connectionDidDie:(NSNotification*)notification -{ -#pragma unused(notification) - _remoteEnd = nil; - [_delegate connectionDidDie]; - - [self scheduleRetry]; -} - -#pragma mark - Communication logic - -- (void)sendMessage:(NSData*)msg -{ - NSString *answer = [[NSString alloc] initWithData:msg encoding:NSUTF8StringEncoding]; - - // Cut the trailing newline. We always only receive one line from the client. - answer = [answer substringToIndex:[answer length] - 1]; - NSArray *chunks = [answer componentsSeparatedByString: @":"]; - - if( [[chunks objectAtIndex:0] isEqualToString:@"STATUS"] ) { - NSString *result = [chunks objectAtIndex:1]; - NSString *path = [chunks objectAtIndex:2]; - if( [chunks count] > 3 ) { - for( int i = 2; i < [chunks count]-1; i++ ) { - path = [NSString stringWithFormat:@"%@:%@", - path, [chunks objectAtIndex:i+1] ]; - } - } - [_delegate setResultForPath:path result:result]; - } else if( [[chunks objectAtIndex:0] isEqualToString:@"UPDATE_VIEW"] ) { - NSString *path = [chunks objectAtIndex:1]; - [_delegate reFetchFileNameCacheForPath:path]; - } else if( [[chunks objectAtIndex:0 ] isEqualToString:@"REGISTER_PATH"] ) { - NSString *path = [chunks objectAtIndex:1]; - [_delegate registerPath:path]; - } else if( [[chunks objectAtIndex:0 ] isEqualToString:@"UNREGISTER_PATH"] ) { - NSString *path = [chunks objectAtIndex:1]; - [_delegate unregisterPath:path]; - } else if( [[chunks objectAtIndex:0 ] isEqualToString:@"GET_STRINGS"] ) { - // BEGIN and END messages, do nothing. - } else if( [[chunks objectAtIndex:0 ] isEqualToString:@"STRING"] ) { - [_delegate setString:[chunks objectAtIndex:1] value:[chunks objectAtIndex:2]]; - } else if( [[chunks objectAtIndex:0 ] isEqualToString:@"GET_MENU_ITEMS"] ) { - if ([[chunks objectAtIndex:1] isEqualToString:@"BEGIN"]) { - [_delegate resetMenuItems]; - } else if ([[chunks objectAtIndex:1] isEqualToString:@"END"]) { - // Don't do anything special, the askOnSocket call in FinderSync menuForMenuKind will return after this line - } - } else if( [[chunks objectAtIndex:0 ] isEqualToString:@"MENU_ITEM"] ) { - NSMutableDictionary *item = [[NSMutableDictionary alloc] init]; - [item setValue:[chunks objectAtIndex:1] forKey:@"command"]; // e.g. "COPY_PRIVATE_LINK" - [item setValue:[chunks objectAtIndex:2] forKey:@"flags"]; // e.g. "d" - [item setValue:[chunks objectAtIndex:3] forKey:@"text"]; // e.g. "Copy private link to clipboard" - [_delegate addMenuItem:item]; - } else { - NSLog(@"SyncState: Unknown command %@", [chunks objectAtIndex:0]); - } -} - -- (void)askOnSocket:(NSString*)path query:(NSString*)verb -{ - NSString *query = [NSString stringWithFormat:@"%@:%@\n", verb,path]; - - @try { - [_remoteEnd sendMessage:[query dataUsingEncoding:NSUTF8StringEncoding]]; - } @catch(NSException* e) { - // Do nothing and wait for connectionDidDie - } -} - -- (void)askForIcon:(NSString*)path isDirectory:(BOOL)isDir -{ - NSString *verb = isDir ? @"RETRIEVE_FOLDER_STATUS" : @"RETRIEVE_FILE_STATUS"; - [self askOnSocket:path query:verb]; -} - -@end - diff --git a/shell_integration/MacOSX/OwnCloudFinderSync/OwnCloudFinderSync.xcodeproj/project.pbxproj b/shell_integration/MacOSX/OwnCloudFinderSync/OwnCloudFinderSync.xcodeproj/project.pbxproj index 5d987c86f..efb88bb56 100644 --- a/shell_integration/MacOSX/OwnCloudFinderSync/OwnCloudFinderSync.xcodeproj/project.pbxproj +++ b/shell_integration/MacOSX/OwnCloudFinderSync/OwnCloudFinderSync.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 03559BDE2736D71B0008FD8E /* LineProcessorV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03559BDD2736D71B0008FD8E /* LineProcessorV1.swift */; }; + 03DB1B202733F6A70026E647 /* LocalSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DB1B1F2733F6A70026E647 /* LocalSocketClient.swift */; }; C2B573BA1B1CD91E00303B36 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C2B573B91B1CD91E00303B36 /* main.m */; }; C2B573D21B1CD94B00303B36 /* main.m in Resources */ = {isa = PBXBuildFile; fileRef = C2B573B91B1CD91E00303B36 /* main.m */; }; C2B573DE1B1CD9CE00303B36 /* FinderSync.m in Sources */ = {isa = PBXBuildFile; fileRef = C2B573DD1B1CD9CE00303B36 /* FinderSync.m */; }; @@ -16,7 +18,6 @@ C2B573F51B1DAD6400303B36 /* ok.iconset in Resources */ = {isa = PBXBuildFile; fileRef = C2B573ED1B1DAD6400303B36 /* ok.iconset */; }; C2B573F71B1DAD6400303B36 /* sync.iconset in Resources */ = {isa = PBXBuildFile; fileRef = C2B573EF1B1DAD6400303B36 /* sync.iconset */; }; C2B573F91B1DAD6400303B36 /* warning.iconset in Resources */ = {isa = PBXBuildFile; fileRef = C2B573F11B1DAD6400303B36 /* warning.iconset */; }; - C2C932F01F0BFC6700C8BCB3 /* SyncClientProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = C2C932EF1F0BFC6700C8BCB3 /* SyncClientProxy.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -44,6 +45,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 03559BDD2736D71B0008FD8E /* LineProcessorV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineProcessorV1.swift; sourceTree = ""; }; + 03DB1B1E2733F6A70026E647 /* FinderSyncExt-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FinderSyncExt-Bridging-Header.h"; sourceTree = ""; }; + 03DB1B1F2733F6A70026E647 /* LocalSocketClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalSocketClient.swift; sourceTree = ""; }; C2B573B11B1CD91E00303B36 /* desktopclient.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktopclient.app; sourceTree = BUILT_PRODUCTS_DIR; }; C2B573B51B1CD91E00303B36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C2B573B91B1CD91E00303B36 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -57,8 +61,7 @@ C2B573ED1B1DAD6400303B36 /* ok.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; name = ok.iconset; path = ../../icons/nopadding/ok.iconset; sourceTree = SOURCE_ROOT; }; C2B573EF1B1DAD6400303B36 /* sync.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; name = sync.iconset; path = ../../icons/nopadding/sync.iconset; sourceTree = SOURCE_ROOT; }; C2B573F11B1DAD6400303B36 /* warning.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; name = warning.iconset; path = ../../icons/nopadding/warning.iconset; sourceTree = SOURCE_ROOT; }; - C2C932EE1F0BFC6700C8BCB3 /* SyncClientProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SyncClientProxy.h; sourceTree = ""; }; - C2C932EF1F0BFC6700C8BCB3 /* SyncClientProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SyncClientProxy.m; sourceTree = ""; }; + C2C932EE1F0BFC6700C8BCB3 /* SyncClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SyncClient.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -117,11 +120,13 @@ C2B573D81B1CD9CE00303B36 /* FinderSyncExt */ = { isa = PBXGroup; children = ( - C2C932EE1F0BFC6700C8BCB3 /* SyncClientProxy.h */, - C2C932EF1F0BFC6700C8BCB3 /* SyncClientProxy.m */, + C2C932EE1F0BFC6700C8BCB3 /* SyncClient.h */, C2B573DC1B1CD9CE00303B36 /* FinderSync.h */, C2B573DD1B1CD9CE00303B36 /* FinderSync.m */, C2B573D91B1CD9CE00303B36 /* Supporting Files */, + 03DB1B1F2733F6A70026E647 /* LocalSocketClient.swift */, + 03559BDD2736D71B0008FD8E /* LineProcessorV1.swift */, + 03DB1B1E2733F6A70026E647 /* FinderSyncExt-Bridging-Header.h */, ); path = FinderSyncExt; sourceTree = ""; @@ -195,6 +200,7 @@ C2B573D61B1CD9CE00303B36 = { CreatedOnToolsVersion = 6.3.1; DevelopmentTeam = 9B5WD74GWJ; + LastSwiftMigration = 1310; SystemCapabilities = { com.apple.ApplicationGroups.Mac = { enabled = 1; @@ -275,8 +281,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C2C932F01F0BFC6700C8BCB3 /* SyncClientProxy.m in Sources */, + 03DB1B202733F6A70026E647 /* LocalSocketClient.swift in Sources */, C2B573DE1B1CD9CE00303B36 /* FinderSync.m in Sources */, + 03559BDE2736D71B0008FD8E /* LineProcessorV1.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -294,18 +301,21 @@ C2B573991B1CD88000303B36 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + MACOSX_DEPLOYMENT_TARGET = 10.12; }; name = Debug; }; C2B5739A1B1CD88000303B36 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + MACOSX_DEPLOYMENT_TARGET = 10.12; }; name = Release; }; C2B573CD1B1CD91E00303B36 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; @@ -343,7 +353,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = desktopclient/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -355,6 +365,7 @@ C2B573CE1B1CD91E00303B36 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; @@ -386,7 +397,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = desktopclient/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -434,7 +445,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = FinderSyncExt/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = YES; OC_APPLICATION_NAME = ownCloud; OC_APPLICATION_REV_DOMAIN = com.owncloud.desktopclient; @@ -445,6 +456,9 @@ PROVISIONING_PROFILE = ""; SDKROOT = macosx; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "FinderSyncExt/FinderSyncExt-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -483,7 +497,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = FinderSyncExt/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 10.12; MTL_ENABLE_DEBUG_INFO = NO; OC_APPLICATION_NAME = ownCloud; OC_APPLICATION_REV_DOMAIN = com.owncloud.desktopclient; @@ -493,6 +507,8 @@ PROVISIONING_PROFILE = ""; SDKROOT = macosx; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "FinderSyncExt/FinderSyncExt-Bridging-Header.h"; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/src/gui/guiutility_mac.mm b/src/gui/guiutility_mac.mm index 65e05ab6a..0aa7e114e 100644 --- a/src/gui/guiutility_mac.mm +++ b/src/gui/guiutility_mac.mm @@ -19,6 +19,7 @@ #include #import +#import namespace OCC { @@ -54,9 +55,12 @@ void Utility::startShellIntegration() QString Utility::socketApiSocketPath() { // This must match the code signing Team setting of the extension - // Example for developer builds (with ad-hoc signing identity): "" "com.owncloud.desktopclient" ".socketApi" - // Example for official signed packages: "9B5WD74GWJ." "com.owncloud.desktopclient" ".socketApi" - return QLatin1String(SOCKETAPI_TEAM_IDENTIFIER_PREFIX "." APPLICATION_REV_DOMAIN ".socketApi"); + // Example for all builds: "9B5WD74GWJ" "." "com.owncloud.desktopclient" + NSString *appGroupId = @SOCKETAPI_TEAM_IDENTIFIER_PREFIX "." APPLICATION_REV_DOMAIN; + + NSURL *container = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupId]; + NSURL *socketPath = [container URLByAppendingPathComponent:@"GUI.socket" isDirectory:false]; + return QString::fromNSString(socketPath.path); } } // namespace OCC diff --git a/src/gui/socketapi/CMakeLists.txt b/src/gui/socketapi/CMakeLists.txt index 78ce96cfd..0f28db92e 100644 --- a/src/gui/socketapi/CMakeLists.txt +++ b/src/gui/socketapi/CMakeLists.txt @@ -2,7 +2,3 @@ target_sources(owncloudCore PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/socketapi.cpp ${CMAKE_CURRENT_SOURCE_DIR}/socketuploadjob.cpp ) - -if( APPLE ) - target_sources(owncloudCore PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/socketapisocket_mac.mm) -endif() diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index c1dad9184..e83166932 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -37,37 +37,30 @@ #include "syncfileitem.h" #include "theme.h" -#include +#include +#include #include -#include -#include -#include -#include -#include -#include +#include +#include #include -#include -#include -#include -#include +#include #include - - -#include #include #include #include -#include -#include - -#include - +#include +#include +#include +#include #include +#include #include +#include +#include +#include +#include -#ifdef Q_OS_MAC -#include -#endif +#include // This is the version that is returned when the client asks for the VERSION. diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h index 84229b1dd..1fc8e4546 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -22,7 +22,7 @@ #include "config.h" -#if defined(Q_OS_MAC) +#if defined(Q_OS_MAC) && 0 #include "socketapisocket_mac.h" #else #include diff --git a/src/gui/socketapi/socketapisocket_mac.h b/src/gui/socketapi/socketapisocket_mac.h deleted file mode 100644 index cbacdf028..000000000 --- a/src/gui/socketapi/socketapisocket_mac.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) by Jocelyn Turcotte - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - */ - -#ifndef SOCKETAPISOCKET_OSX_H -#define SOCKETAPISOCKET_OSX_H - -#include -#include - -class SocketApiServerPrivate; -class SocketApiSocketPrivate; - -class SocketApiSocket : public QIODevice -{ - Q_OBJECT -public: - SocketApiSocket(QObject *parent, SocketApiSocketPrivate *p); - ~SocketApiSocket() override; - - qint64 readData(char *data, qint64 maxlen) override; - qint64 writeData(const char *data, qint64 len) override; - - bool isSequential() const override { return true; } - qint64 bytesAvailable() const override; - bool canReadLine() const override; - -signals: - void disconnected(); - -private: - // Use Qt's p-impl system to hide objective-c types from C++ code including this file - Q_DECLARE_PRIVATE(SocketApiSocket) - QScopedPointer d_ptr; - friend class SocketApiServerPrivate; -}; - -class SocketApiServer : public QObject -{ - Q_OBJECT -public: - SocketApiServer(); - ~SocketApiServer() override; - - void close(); - bool listen(const QString &name); - SocketApiSocket *nextPendingConnection(); - - static bool removeServer(const QString &) { return false; } - -signals: - void newConnection(); - -private: - Q_DECLARE_PRIVATE(SocketApiServer) - QScopedPointer d_ptr; -}; - -#endif // SOCKETAPISOCKET_OSX_H diff --git a/src/gui/socketapi/socketapisocket_mac.mm b/src/gui/socketapi/socketapisocket_mac.mm deleted file mode 100644 index 938494e21..000000000 --- a/src/gui/socketapi/socketapisocket_mac.mm +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright (C) by Jocelyn Turcotte - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - */ - -#include "socketapisocket_mac.h" -#import - -@protocol ChannelProtocol - -- (void)sendMessage:(NSData *)msg; - -@end - -@protocol RemoteEndProtocol - -- (void)registerTransmitter:(id)tx; - -@end - -@interface LocalEnd : NSObject - -@property (atomic) SocketApiSocketPrivate *wrapper; - -- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper; - -@end - -@interface Server : NSObject - -@property (atomic) SocketApiServerPrivate *wrapper; - -- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper; -- (void)registerClient:(NSDistantObject *)remoteEnd; - -@end - -class SocketApiSocketPrivate -{ -public: - SocketApiSocket *q_ptr; - - SocketApiSocketPrivate(NSDistantObject *remoteEnd); - ~SocketApiSocketPrivate(); - - // release remoteEnd - void disconnectRemote(); - - NSDistantObject *remoteEnd; - LocalEnd *localEnd; - QByteArray inBuffer; - bool isRemoteDisconnected = false; -}; - -class SocketApiServerPrivate -{ -public: - SocketApiServer *q_ptr; - - SocketApiServerPrivate(); - ~SocketApiServerPrivate(); - - QList pendingConnections; - NSConnection *connection; - Server *server; -}; - - -@implementation LocalEnd - -@synthesize wrapper = _wrapper; - -- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper -{ - self = [super init]; - self.wrapper = wrapper; - return self; -} - -- (void)sendMessage:(NSData *)msg -{ - if (self.wrapper) { - self.wrapper->inBuffer += QByteArray::fromRawNSData(msg); - emit self.wrapper->q_ptr->readyRead(); - } -} - -- (void)connectionDidDie:(NSNotification *)notification -{ - // The NSConnectionDidDieNotification docs say to disconnect from NSConnection here - [[NSNotificationCenter defaultCenter] removeObserver:self]; - - if (self.wrapper) { - self.wrapper->disconnectRemote(); - emit self.wrapper->q_ptr->disconnected(); - } -} -@end - -@implementation Server - -@synthesize wrapper = _wrapper; - -- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper -{ - self = [super init]; - self.wrapper = wrapper; - return self; -} - -- (void)registerClient:(NSDistantObject *)remoteEnd -{ - // This saves a few mach messages that would otherwise be needed to query the interface - [remoteEnd setProtocolForProxy:@protocol(RemoteEndProtocol)]; - - SocketApiServer *server = self.wrapper->q_ptr; - SocketApiSocketPrivate *socketPrivate = new SocketApiSocketPrivate(remoteEnd); - SocketApiSocket *socket = new SocketApiSocket(server, socketPrivate); - self.wrapper->pendingConnections.append(socket); - emit server->newConnection(); - - [remoteEnd registerTransmitter:socketPrivate->localEnd]; -} -@end - - -SocketApiSocket::SocketApiSocket(QObject *parent, SocketApiSocketPrivate *p) - : QIODevice(parent) - , d_ptr(p) -{ - Q_D(SocketApiSocket); - d->q_ptr = this; - open(ReadWrite); -} - -SocketApiSocket::~SocketApiSocket() -{ -} - -qint64 SocketApiSocket::readData(char *data, qint64 maxlen) -{ - Q_D(SocketApiSocket); - qint64 len = std::min(maxlen, static_cast(d->inBuffer.size())); - if (len < 0 || len > std::numeric_limits::max()) { - return -1; - } - - memcpy(data, d->inBuffer.constData(), static_cast(len)); - d->inBuffer.remove(0, static_cast(len)); - return len; -} - -qint64 SocketApiSocket::writeData(const char *data, qint64 len) -{ - Q_D(SocketApiSocket); - if (d->isRemoteDisconnected) { - return -1; - } - - if (len < std::numeric_limits::min() || len > std::numeric_limits::max()) { - return -1; - } - - @try { - // FIXME: The NSConnection will make this block unless the function is marked as "oneway" - // in the protocol. This isn't async and reduces our performances but this currectly avoids - // a Mach queue deadlock during requests bursts of the legacy OwnCloudFinder extension. - // Since FinderSync already runs in a separate process, blocking isn't too critical. - NSData *payload = QByteArray::fromRawData(data, static_cast(len)).toRawNSData(); - [d->remoteEnd sendMessage:payload]; - return len; - } @catch (NSException *) { - // connectionDidDie can be notified too late, also interpret any sending exception as a disconnection. - d->disconnectRemote(); - emit disconnected(); - return -1; - } -} - -qint64 SocketApiSocket::bytesAvailable() const -{ - Q_D(const SocketApiSocket); - return d->inBuffer.size() + QIODevice::bytesAvailable(); -} - -bool SocketApiSocket::canReadLine() const -{ - Q_D(const SocketApiSocket); - return d->inBuffer.indexOf('\n', int(pos())) != -1 || QIODevice::canReadLine(); -} - -SocketApiSocketPrivate::SocketApiSocketPrivate(NSDistantObject *remoteEnd) - : remoteEnd(remoteEnd) - , localEnd([[LocalEnd alloc] initWithWrapper:this]) -{ - [remoteEnd retain]; - // (Ab)use our objective-c object just to catch the notification - [[NSNotificationCenter defaultCenter] addObserver:localEnd - selector:@selector(connectionDidDie:) - name:NSConnectionDidDieNotification - object:[remoteEnd connectionForProxy]]; -} - -SocketApiSocketPrivate::~SocketApiSocketPrivate() -{ - disconnectRemote(); - - // The DO vended localEnd might still be referenced by the connection - localEnd.wrapper = nil; - [localEnd release]; -} - -void SocketApiSocketPrivate::disconnectRemote() -{ - if (isRemoteDisconnected) - return; - isRemoteDisconnected = true; - - [remoteEnd release]; -} - -SocketApiServer::SocketApiServer() - : d_ptr(new SocketApiServerPrivate) -{ - Q_D(SocketApiServer); - d->q_ptr = this; -} - -SocketApiServer::~SocketApiServer() -{ -} - -void SocketApiServer::close() -{ - // Assume we'll be destroyed right after -} - -bool SocketApiServer::listen(const QString &name) -{ - Q_D(SocketApiServer); - // Set the name of the root object - return [d->connection registerName:name.toNSString()]; -} - -SocketApiSocket *SocketApiServer::nextPendingConnection() -{ - Q_D(SocketApiServer); - return d->pendingConnections.takeFirst(); -} - -SocketApiServerPrivate::SocketApiServerPrivate() -{ - // Create the connection and server object to vend over Disributed Objects - connection = [[NSConnection alloc] init]; - server = [[Server alloc] initWithWrapper:this]; - [connection setRootObject:server]; -} - -SocketApiServerPrivate::~SocketApiServerPrivate() -{ - [connection release]; - server.wrapper = nil; - [server release]; -} -- cgit v1.2.3