electron_bundle_mover.mm 16 KB

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