atom_bundle_mover.mm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. // Copyright (c) 2017 GitHub, Inc.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #import "atom/browser/ui/cocoa/atom_bundle_mover.h"
  5. #import <AppKit/AppKit.h>
  6. #import <Foundation/Foundation.h>
  7. #import <Security/Security.h>
  8. #import <dlfcn.h>
  9. #import <sys/mount.h>
  10. #import <sys/param.h>
  11. #import "atom/browser/browser.h"
  12. namespace atom {
  13. namespace ui {
  14. namespace cocoa {
  15. bool AtomBundleMover::Move(mate::Arguments* args) {
  16. // Path of the current bundle
  17. NSString* bundlePath = [[NSBundle mainBundle] bundlePath];
  18. // Skip if the application is already in the Applications folder
  19. if (IsInApplicationsFolder(bundlePath))
  20. return true;
  21. NSFileManager* fileManager = [NSFileManager defaultManager];
  22. NSString* diskImageDevice = ContainingDiskImageDevice(bundlePath);
  23. NSString* applicationsDirectory = [[NSSearchPathForDirectoriesInDomains(
  24. NSApplicationDirectory, NSLocalDomainMask, true) lastObject]
  25. stringByResolvingSymlinksInPath];
  26. NSString* bundleName = [bundlePath lastPathComponent];
  27. NSString* destinationPath =
  28. [applicationsDirectory stringByAppendingPathComponent:bundleName];
  29. // Check if we can write to the applications directory
  30. // and then make sure that if the app already exists we can overwrite it
  31. bool needAuthorization =
  32. ![fileManager isWritableFileAtPath:applicationsDirectory] |
  33. ([fileManager fileExistsAtPath:destinationPath] &&
  34. ![fileManager isWritableFileAtPath:destinationPath]);
  35. // Activate app -- work-around for focus issues related to "scary file from
  36. // internet" OS dialog.
  37. if (![NSApp isActive]) {
  38. [NSApp activateIgnoringOtherApps:true];
  39. }
  40. // Move to applications folder
  41. if (needAuthorization) {
  42. bool authorizationCanceled;
  43. if (!AuthorizedInstall(bundlePath, destinationPath,
  44. &authorizationCanceled)) {
  45. if (authorizationCanceled) {
  46. // User rejected the authorization request
  47. args->ThrowError("User rejected the authorization request");
  48. return false;
  49. } else {
  50. args->ThrowError(
  51. "Failed to copy to applications directory even with authorization");
  52. return false;
  53. }
  54. }
  55. } else {
  56. // If a copy already exists in the Applications folder, put it in the Trash
  57. if ([fileManager fileExistsAtPath:destinationPath]) {
  58. // But first, make sure that it's not running
  59. if (IsApplicationAtPathRunning(destinationPath)) {
  60. // Give the running app focus and terminate myself
  61. [[NSTask
  62. launchedTaskWithLaunchPath:@"/usr/bin/open"
  63. arguments:[NSArray
  64. arrayWithObject:destinationPath]]
  65. waitUntilExit];
  66. atom::Browser::Get()->Quit();
  67. return true;
  68. } else {
  69. if (!Trash([applicationsDirectory
  70. stringByAppendingPathComponent:bundleName])) {
  71. args->ThrowError("Failed to delete existing application");
  72. return false;
  73. }
  74. }
  75. }
  76. if (!CopyBundle(bundlePath, destinationPath)) {
  77. args->ThrowError(
  78. "Failed to copy current bundle to the applications folder");
  79. return false;
  80. }
  81. }
  82. // Trash the original app. It's okay if this fails.
  83. // NOTE: This final delete does not work if the source bundle is in a network
  84. // mounted volume.
  85. // Calling rm or file manager's delete method doesn't work either. It's
  86. // unlikely to happen but it'd be great if someone could fix this.
  87. if (diskImageDevice == nil && !DeleteOrTrash(bundlePath)) {
  88. // Could not delete original but we just don't care
  89. }
  90. // Relaunch.
  91. Relaunch(destinationPath);
  92. // Launched from within a disk image? -- unmount (if no files are open after 5
  93. // seconds, otherwise leave it mounted).
  94. if (diskImageDevice) {
  95. NSString* script = [NSString
  96. stringWithFormat:@"(/bin/sleep 5 && /usr/bin/hdiutil detach %@) &",
  97. ShellQuotedString(diskImageDevice)];
  98. [NSTask launchedTaskWithLaunchPath:@"/bin/sh"
  99. arguments:[NSArray arrayWithObjects:@"-c", script,
  100. nil]];
  101. }
  102. atom::Browser::Get()->Quit();
  103. return true;
  104. }
  105. bool AtomBundleMover::IsCurrentAppInApplicationsFolder() {
  106. return IsInApplicationsFolder([[NSBundle mainBundle] bundlePath]);
  107. }
  108. bool AtomBundleMover::IsInApplicationsFolder(NSString* bundlePath) {
  109. // Check all the normal Application directories
  110. NSArray* applicationDirs = NSSearchPathForDirectoriesInDomains(
  111. NSApplicationDirectory, NSAllDomainsMask, true);
  112. for (NSString* appDir in applicationDirs) {
  113. if ([bundlePath hasPrefix:appDir])
  114. return true;
  115. }
  116. // Also, handle the case that the user has some other Application directory
  117. // (perhaps on a separate data partition).
  118. if ([[bundlePath pathComponents] containsObject:@"Applications"])
  119. return true;
  120. return false;
  121. }
  122. NSString* AtomBundleMover::ContainingDiskImageDevice(NSString* bundlePath) {
  123. NSString* containingPath = [bundlePath stringByDeletingLastPathComponent];
  124. struct statfs fs;
  125. if (statfs([containingPath fileSystemRepresentation], &fs) ||
  126. (fs.f_flags & MNT_ROOTFS))
  127. return nil;
  128. NSString* device = [[NSFileManager defaultManager]
  129. stringWithFileSystemRepresentation:fs.f_mntfromname
  130. length:strlen(fs.f_mntfromname)];
  131. NSTask* hdiutil = [[[NSTask alloc] init] autorelease];
  132. [hdiutil setLaunchPath:@"/usr/bin/hdiutil"];
  133. [hdiutil setArguments:[NSArray arrayWithObjects:@"info", @"-plist", nil]];
  134. [hdiutil setStandardOutput:[NSPipe pipe]];
  135. [hdiutil launch];
  136. [hdiutil waitUntilExit];
  137. NSData* data =
  138. [[[hdiutil standardOutput] fileHandleForReading] readDataToEndOfFile];
  139. NSDictionary* info = nil;
  140. if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_5) {
  141. info = [NSPropertyListSerialization
  142. propertyListWithData:data
  143. options:NSPropertyListImmutable
  144. format:NULL
  145. error:NULL];
  146. } else {
  147. info = [NSPropertyListSerialization
  148. propertyListFromData:data
  149. mutabilityOption:NSPropertyListImmutable
  150. format:NULL
  151. errorDescription:NULL];
  152. }
  153. if (![info isKindOfClass:[NSDictionary class]])
  154. return nil;
  155. NSArray* images = (NSArray*)[info objectForKey:@"images"];
  156. if (![images isKindOfClass:[NSArray class]])
  157. return nil;
  158. for (NSDictionary* image in images) {
  159. if (![image isKindOfClass:[NSDictionary class]])
  160. return nil;
  161. id systemEntities = [image objectForKey:@"system-entities"];
  162. if (![systemEntities isKindOfClass:[NSArray class]])
  163. return nil;
  164. for (NSDictionary* systemEntity in systemEntities) {
  165. if (![systemEntity isKindOfClass:[NSDictionary class]])
  166. return nil;
  167. NSString* devEntry = [systemEntity objectForKey:@"dev-entry"];
  168. if (![devEntry isKindOfClass:[NSString class]])
  169. return nil;
  170. if ([devEntry isEqualToString:device])
  171. return device;
  172. }
  173. }
  174. return nil;
  175. }
  176. bool AtomBundleMover::AuthorizedInstall(NSString* srcPath,
  177. NSString* dstPath,
  178. bool* canceled) {
  179. if (canceled)
  180. *canceled = false;
  181. // Make sure that the destination path is an app bundle. We're essentially
  182. // running 'sudo rm -rf' so we really don't want to screw this up.
  183. if (![[dstPath pathExtension] isEqualToString:@"app"])
  184. return false;
  185. // Do some more checks
  186. if ([[dstPath stringByTrimmingCharactersInSet:[NSCharacterSet
  187. whitespaceCharacterSet]]
  188. length] == 0)
  189. return false;
  190. if ([[srcPath stringByTrimmingCharactersInSet:[NSCharacterSet
  191. whitespaceCharacterSet]]
  192. length] == 0)
  193. return false;
  194. int pid, status;
  195. AuthorizationRef myAuthorizationRef;
  196. // Get the authorization
  197. OSStatus err =
  198. AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment,
  199. kAuthorizationFlagDefaults, &myAuthorizationRef);
  200. if (err != errAuthorizationSuccess)
  201. return false;
  202. AuthorizationItem myItems = {kAuthorizationRightExecute, 0, NULL, 0};
  203. AuthorizationRights myRights = {1, &myItems};
  204. AuthorizationFlags myFlags = (AuthorizationFlags)(
  205. kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights |
  206. kAuthorizationFlagPreAuthorize);
  207. err = AuthorizationCopyRights(myAuthorizationRef, &myRights, NULL, myFlags,
  208. NULL);
  209. if (err != errAuthorizationSuccess) {
  210. if (err == errAuthorizationCanceled && canceled)
  211. *canceled = true;
  212. goto fail;
  213. }
  214. static OSStatus (*security_AuthorizationExecuteWithPrivileges)(
  215. AuthorizationRef authorization, const char* pathToTool,
  216. AuthorizationFlags options, char* const* arguments,
  217. FILE** communicationsPipe) = NULL;
  218. if (!security_AuthorizationExecuteWithPrivileges) {
  219. // On 10.7, AuthorizationExecuteWithPrivileges is deprecated. We want to
  220. // still use it since there's no good alternative (without requiring code
  221. // signing). We'll look up the function through dyld and fail if it is no
  222. // longer accessible. If Apple removes the function entirely this will fail
  223. // gracefully. If they keep the function and throw some sort of exception,
  224. // this won't fail gracefully, but that's a risk we'll have to take for now.
  225. security_AuthorizationExecuteWithPrivileges = (OSStatus(*)(
  226. AuthorizationRef, const char*, AuthorizationFlags, char* const*,
  227. FILE**))dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges");
  228. }
  229. if (!security_AuthorizationExecuteWithPrivileges)
  230. goto fail;
  231. // Delete the destination
  232. {
  233. char rf[] = "-rf";
  234. char* args[] = {rf, (char*)[dstPath fileSystemRepresentation], NULL};
  235. err = security_AuthorizationExecuteWithPrivileges(
  236. myAuthorizationRef, "/bin/rm", kAuthorizationFlagDefaults, args, NULL);
  237. if (err != errAuthorizationSuccess)
  238. goto fail;
  239. // Wait until it's done
  240. pid = wait(&status);
  241. if (pid == -1 || !WIFEXITED(status))
  242. goto fail; // We don't care about exit status as the destination most
  243. // likely does not exist
  244. }
  245. // Copy
  246. {
  247. char pR[] = "-pR";
  248. char* args[] = {pR, (char*)[srcPath fileSystemRepresentation],
  249. (char*)[dstPath fileSystemRepresentation], NULL};
  250. err = security_AuthorizationExecuteWithPrivileges(
  251. myAuthorizationRef, "/bin/cp", kAuthorizationFlagDefaults, args, NULL);
  252. if (err != errAuthorizationSuccess)
  253. goto fail;
  254. // Wait until it's done
  255. pid = wait(&status);
  256. if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status))
  257. goto fail;
  258. }
  259. AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDefaults);
  260. return true;
  261. fail:
  262. AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDefaults);
  263. return false;
  264. }
  265. bool AtomBundleMover::CopyBundle(NSString* srcPath, NSString* dstPath) {
  266. NSFileManager* fileManager = [NSFileManager defaultManager];
  267. NSError* error = nil;
  268. if ([fileManager copyItemAtPath:srcPath toPath:dstPath error:&error]) {
  269. return true;
  270. } else {
  271. return false;
  272. }
  273. }
  274. NSString* AtomBundleMover::ShellQuotedString(NSString* string) {
  275. return [NSString
  276. stringWithFormat:@"'%@'",
  277. [string stringByReplacingOccurrencesOfString:@"'"
  278. withString:@"'\\''"]];
  279. }
  280. void AtomBundleMover::Relaunch(NSString* destinationPath) {
  281. // The shell script waits until the original app process terminates.
  282. // This is done so that the relaunched app opens as the front-most app.
  283. int pid = [[NSProcessInfo processInfo] processIdentifier];
  284. // Command run just before running open /final/path
  285. NSString* preOpenCmd = @"";
  286. NSString* quotedDestinationPath = ShellQuotedString(destinationPath);
  287. // Before we launch the new app, clear xattr:com.apple.quarantine to avoid
  288. // duplicate "scary file from the internet" dialog.
  289. preOpenCmd = [NSString
  290. stringWithFormat:@"/usr/bin/xattr -d -r com.apple.quarantine %@",
  291. quotedDestinationPath];
  292. NSString* script =
  293. [NSString stringWithFormat:
  294. @"(while /bin/kill -0 %d >&/dev/null; do /bin/sleep 0.1; "
  295. @"done; %@; /usr/bin/open %@) &",
  296. pid, preOpenCmd, quotedDestinationPath];
  297. [NSTask
  298. launchedTaskWithLaunchPath:@"/bin/sh"
  299. arguments:[NSArray arrayWithObjects:@"-c", script, nil]];
  300. }
  301. bool AtomBundleMover::IsApplicationAtPathRunning(NSString* bundlePath) {
  302. bundlePath = [bundlePath stringByStandardizingPath];
  303. for (NSRunningApplication* runningApplication in
  304. [[NSWorkspace sharedWorkspace] runningApplications]) {
  305. NSString* runningAppBundlePath =
  306. [[[runningApplication bundleURL] path] stringByStandardizingPath];
  307. if ([runningAppBundlePath isEqualToString:bundlePath]) {
  308. return true;
  309. }
  310. }
  311. return false;
  312. }
  313. bool AtomBundleMover::Trash(NSString* path) {
  314. bool result = false;
  315. if (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_8) {
  316. result = [[NSFileManager defaultManager]
  317. trashItemAtURL:[NSURL fileURLWithPath:path]
  318. resultingItemURL:NULL
  319. error:NULL];
  320. }
  321. if (!result) {
  322. result = [[NSWorkspace sharedWorkspace]
  323. performFileOperation:NSWorkspaceRecycleOperation
  324. source:[path stringByDeletingLastPathComponent]
  325. destination:@""
  326. files:[NSArray arrayWithObject:[path lastPathComponent]]
  327. tag:NULL];
  328. }
  329. // As a last resort try trashing with AppleScript.
  330. // This allows us to trash the app in macOS Sierra even when the app is
  331. // running inside an app translocation image.
  332. if (!result) {
  333. NSAppleScript* appleScript = [[[NSAppleScript alloc]
  334. initWithSource:
  335. [NSString
  336. stringWithFormat:
  337. @"\
  338. set theFile to POSIX file \"%@\" "
  339. @"\n\
  340. tell application \"Finder\" "
  341. @"\n\
  342. move theFile to trash \n\
  343. "
  344. @" end tell",
  345. path]] autorelease];
  346. NSDictionary* errorDict = nil;
  347. NSAppleEventDescriptor* scriptResult =
  348. [appleScript executeAndReturnError:&errorDict];
  349. result = (scriptResult != nil);
  350. }
  351. return result;
  352. }
  353. bool AtomBundleMover::DeleteOrTrash(NSString* path) {
  354. NSError* error;
  355. if ([[NSFileManager defaultManager] removeItemAtPath:path error:&error]) {
  356. return true;
  357. } else {
  358. return Trash(path);
  359. }
  360. }
  361. } // namespace cocoa
  362. } // namespace ui
  363. } // namespace atom