atom-application.js
1 const AtomWindow = require('./atom-window'); 2 const ApplicationMenu = require('./application-menu'); 3 const AtomProtocolHandler = require('./atom-protocol-handler'); 4 const AutoUpdateManager = require('./auto-update-manager'); 5 const StorageFolder = require('../storage-folder'); 6 const Config = require('../config'); 7 const ConfigFile = require('../config-file'); 8 const FileRecoveryService = require('./file-recovery-service'); 9 const StartupTime = require('../startup-time'); 10 const ipcHelpers = require('../ipc-helpers'); 11 const { 12 BrowserWindow, 13 Menu, 14 app, 15 clipboard, 16 dialog, 17 ipcMain, 18 shell, 19 screen 20 } = require('electron'); 21 const { CompositeDisposable, Disposable } = require('event-kit'); 22 const crypto = require('crypto'); 23 const fs = require('fs-plus'); 24 const path = require('path'); 25 const os = require('os'); 26 const net = require('net'); 27 const url = require('url'); 28 const { promisify } = require('util'); 29 const { EventEmitter } = require('events'); 30 const _ = require('underscore-plus'); 31 let FindParentDir = null; 32 let Resolve = null; 33 const ConfigSchema = require('../config-schema'); 34 35 const LocationSuffixRegExp = /(:\d+)(:\d+)?$/; 36 37 // Increment this when changing the serialization format of `${ATOM_HOME}/storage/application.json` used by 38 // AtomApplication::saveCurrentWindowOptions() and AtomApplication::loadPreviousWindowOptions() in a backward- 39 // incompatible way. 40 const APPLICATION_STATE_VERSION = '1'; 41 42 const getDefaultPath = () => { 43 const editor = atom.workspace.getActiveTextEditor(); 44 if (!editor || !editor.getPath()) { 45 return; 46 } 47 const paths = atom.project.getPaths(); 48 if (paths) { 49 return paths[0]; 50 } 51 }; 52 53 const getSocketSecretPath = atomVersion => { 54 const { username } = os.userInfo(); 55 const atomHome = path.resolve(process.env.ATOM_HOME); 56 57 return path.join(atomHome, `.atom-socket-secret-${username}-${atomVersion}`); 58 }; 59 60 const getSocketPath = socketSecret => { 61 if (!socketSecret) { 62 return null; 63 } 64 65 // Hash the secret to create the socket name to not expose it. 66 const socketName = crypto 67 .createHmac('sha256', socketSecret) 68 .update('socketName') 69 .digest('hex') 70 .substr(0, 12); 71 72 if (process.platform === 'win32') { 73 return `\\\\.\\pipe\\atom-${socketName}-sock`; 74 } else { 75 return path.join(os.tmpdir(), `atom-${socketName}.sock`); 76 } 77 }; 78 79 const getExistingSocketSecret = atomVersion => { 80 const socketSecretPath = getSocketSecretPath(atomVersion); 81 82 if (!fs.existsSync(socketSecretPath)) { 83 return null; 84 } 85 86 return fs.readFileSync(socketSecretPath, 'utf8'); 87 }; 88 89 const getRandomBytes = promisify(crypto.randomBytes); 90 const writeFile = promisify(fs.writeFile); 91 92 const createSocketSecret = async atomVersion => { 93 const socketSecret = (await getRandomBytes(16)).toString('hex'); 94 95 await writeFile(getSocketSecretPath(atomVersion), socketSecret, { 96 encoding: 'utf8', 97 mode: 0o600 98 }); 99 100 return socketSecret; 101 }; 102 103 const encryptOptions = (options, secret) => { 104 const message = JSON.stringify(options); 105 106 // Even if the following IV is not cryptographically secure, there's a really good chance 107 // it's going to be unique between executions which is the requirement for GCM. 108 // We're not using `crypto.randomBytes()` because in electron v2, that API is really slow 109 // on Windows machines, which affects the startup time of Atom. 110 // TodoElectronIssue: Once we upgrade to electron v3 we can use `crypto.randomBytes()` 111 const initVectorHash = crypto.createHash('sha1'); 112 initVectorHash.update(Date.now() + ''); 113 initVectorHash.update(Math.random() + ''); 114 const initVector = initVectorHash.digest(); 115 116 const cipher = crypto.createCipheriv('aes-256-gcm', secret, initVector); 117 118 let content = cipher.update(message, 'utf8', 'hex'); 119 content += cipher.final('hex'); 120 121 const authTag = cipher.getAuthTag().toString('hex'); 122 123 return JSON.stringify({ 124 authTag, 125 content, 126 initVector: initVector.toString('hex') 127 }); 128 }; 129 130 const decryptOptions = (optionsMessage, secret) => { 131 const { authTag, content, initVector } = JSON.parse(optionsMessage); 132 133 const decipher = crypto.createDecipheriv( 134 'aes-256-gcm', 135 secret, 136 Buffer.from(initVector, 'hex') 137 ); 138 decipher.setAuthTag(Buffer.from(authTag, 'hex')); 139 140 let message = decipher.update(content, 'hex', 'utf8'); 141 message += decipher.final('utf8'); 142 143 return JSON.parse(message); 144 }; 145 146 // The application's singleton class. 147 // 148 // It's the entry point into the Atom application and maintains the global state 149 // of the application. 150 // 151 module.exports = class AtomApplication extends EventEmitter { 152 // Public: The entry point into the Atom application. 153 static open(options) { 154 StartupTime.addMarker('main-process:atom-application:open'); 155 156 const socketSecret = getExistingSocketSecret(options.version); 157 const socketPath = getSocketPath(socketSecret); 158 const createApplication = 159 options.createApplication || 160 (async () => { 161 const app = new AtomApplication(options); 162 await app.initialize(options); 163 return app; 164 }); 165 166 // FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely 167 // take a few seconds to trigger 'error' event, it could be a bug of node 168 // or electron, before it's fixed we check the existence of socketPath to 169 // speedup startup. 170 if ( 171 !socketPath || 172 options.test || 173 options.benchmark || 174 options.benchmarkTest || 175 (process.platform !== 'win32' && !fs.existsSync(socketPath)) 176 ) { 177 return createApplication(options); 178 } 179 180 return new Promise(resolve => { 181 const client = net.connect({ path: socketPath }, () => { 182 client.write(encryptOptions(options, socketSecret), () => { 183 client.end(); 184 app.quit(); 185 resolve(null); 186 }); 187 }); 188 189 client.on('error', () => resolve(createApplication(options))); 190 }); 191 } 192 193 exit(status) { 194 app.exit(status); 195 } 196 197 constructor(options) { 198 StartupTime.addMarker('main-process:atom-application:constructor:start'); 199 200 super(); 201 this.quitting = false; 202 this.quittingForUpdate = false; 203 this.getAllWindows = this.getAllWindows.bind(this); 204 this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this); 205 this.resourcePath = options.resourcePath; 206 this.devResourcePath = options.devResourcePath; 207 this.version = options.version; 208 this.devMode = options.devMode; 209 this.safeMode = options.safeMode; 210 this.logFile = options.logFile; 211 this.userDataDir = options.userDataDir; 212 this._killProcess = options.killProcess || process.kill.bind(process); 213 this.waitSessionsByWindow = new Map(); 214 this.windowStack = new WindowStack(); 215 216 this.initializeAtomHome(process.env.ATOM_HOME); 217 218 const configFilePath = fs.existsSync( 219 path.join(process.env.ATOM_HOME, 'config.json') 220 ) 221 ? path.join(process.env.ATOM_HOME, 'config.json') 222 : path.join(process.env.ATOM_HOME, 'config.cson'); 223 224 this.configFile = ConfigFile.at(configFilePath); 225 this.config = new Config({ 226 saveCallback: settings => { 227 if (!this.quitting) { 228 return this.configFile.update(settings); 229 } 230 } 231 }); 232 this.config.setSchema(null, { 233 type: 'object', 234 properties: _.clone(ConfigSchema) 235 }); 236 237 this.fileRecoveryService = new FileRecoveryService( 238 path.join(process.env.ATOM_HOME, 'recovery') 239 ); 240 this.storageFolder = new StorageFolder(process.env.ATOM_HOME); 241 this.autoUpdateManager = new AutoUpdateManager( 242 this.version, 243 options.test || options.benchmark || options.benchmarkTest, 244 this.config 245 ); 246 247 this.disposable = new CompositeDisposable(); 248 this.handleEvents(); 249 250 StartupTime.addMarker('main-process:atom-application:constructor:end'); 251 } 252 253 // This stuff was previously done in the constructor, but we want to be able to construct this object 254 // for testing purposes without booting up the world. As you add tests, feel free to move instantiation 255 // of these various sub-objects into the constructor, but you'll need to remove the side-effects they 256 // perform during their construction, adding an initialize method that you call here. 257 async initialize(options) { 258 StartupTime.addMarker('main-process:atom-application:initialize:start'); 259 260 global.atomApplication = this; 261 262 // DEPRECATED: This can be removed at some point (added in 1.13) 263 // It converts `useCustomTitleBar: true` to `titleBar: "custom"` 264 if ( 265 process.platform === 'darwin' && 266 this.config.get('core.useCustomTitleBar') 267 ) { 268 this.config.unset('core.useCustomTitleBar'); 269 this.config.set('core.titleBar', 'custom'); 270 } 271 272 this.applicationMenu = new ApplicationMenu( 273 this.version, 274 this.autoUpdateManager 275 ); 276 this.atomProtocolHandler = new AtomProtocolHandler( 277 this.resourcePath, 278 this.safeMode 279 ); 280 281 // Don't await for the following method to avoid delaying the opening of a new window. 282 // (we await it just after opening it). 283 // We need to do this because `listenForArgumentsFromNewProcess()` calls `crypto.randomBytes`, 284 // which is really slow on Windows machines. 285 // (TodoElectronIssue: This got fixed in electron v3: https://github.com/electron/electron/issues/2073). 286 let socketServerPromise; 287 if (options.test || options.benchmark || options.benchmarkTest) { 288 socketServerPromise = Promise.resolve(); 289 } else { 290 socketServerPromise = this.listenForArgumentsFromNewProcess(); 291 } 292 293 this.setupDockMenu(); 294 295 const result = await this.launch(options); 296 this.autoUpdateManager.initialize(); 297 await socketServerPromise; 298 299 StartupTime.addMarker('main-process:atom-application:initialize:end'); 300 301 return result; 302 } 303 304 async destroy() { 305 const windowsClosePromises = this.getAllWindows().map(window => { 306 window.close(); 307 return window.closedPromise; 308 }); 309 await Promise.all(windowsClosePromises); 310 this.disposable.dispose(); 311 } 312 313 async launch(options) { 314 if (!this.configFilePromise) { 315 this.configFilePromise = this.configFile.watch().then(disposable => { 316 this.disposable.add(disposable); 317 this.config.onDidChange('core.titleBar', () => this.promptForRestart()); 318 this.config.onDidChange('core.colorProfile', () => 319 this.promptForRestart() 320 ); 321 }); 322 323 // TodoElectronIssue: In electron v2 awaiting the watcher causes some delay 324 // in Windows machines, which affects directly the startup time. 325 if (process.platform !== 'win32') { 326 await this.configFilePromise; 327 } 328 } 329 330 let optionsForWindowsToOpen = []; 331 let shouldReopenPreviousWindows = false; 332 333 if (options.test || options.benchmark || options.benchmarkTest) { 334 optionsForWindowsToOpen.push(options); 335 } else if (options.newWindow) { 336 shouldReopenPreviousWindows = false; 337 } else if ( 338 (options.pathsToOpen && options.pathsToOpen.length > 0) || 339 (options.urlsToOpen && options.urlsToOpen.length > 0) 340 ) { 341 optionsForWindowsToOpen.push(options); 342 shouldReopenPreviousWindows = 343 this.config.get('core.restorePreviousWindowsOnStart') === 'always'; 344 } else { 345 shouldReopenPreviousWindows = 346 this.config.get('core.restorePreviousWindowsOnStart') !== 'no'; 347 } 348 349 if (shouldReopenPreviousWindows) { 350 optionsForWindowsToOpen = [ 351 ...(await this.loadPreviousWindowOptions()), 352 ...optionsForWindowsToOpen 353 ]; 354 } 355 356 if (optionsForWindowsToOpen.length === 0) { 357 optionsForWindowsToOpen.push(options); 358 } 359 360 // Preserve window opening order 361 const windows = []; 362 for (const options of optionsForWindowsToOpen) { 363 windows.push(await this.openWithOptions(options)); 364 } 365 return windows; 366 } 367 368 openWithOptions(options) { 369 const { 370 pathsToOpen, 371 executedFrom, 372 foldersToOpen, 373 urlsToOpen, 374 benchmark, 375 benchmarkTest, 376 test, 377 pidToKillWhenClosed, 378 devMode, 379 safeMode, 380 newWindow, 381 logFile, 382 profileStartup, 383 timeout, 384 clearWindowState, 385 addToLastWindow, 386 preserveFocus, 387 env 388 } = options; 389 390 if (!preserveFocus) { 391 app.focus(); 392 } 393 394 if (test) { 395 return this.runTests({ 396 headless: true, 397 devMode, 398 resourcePath: this.resourcePath, 399 executedFrom, 400 pathsToOpen, 401 logFile, 402 timeout, 403 env 404 }); 405 } else if (benchmark || benchmarkTest) { 406 return this.runBenchmarks({ 407 headless: true, 408 test: benchmarkTest, 409 resourcePath: this.resourcePath, 410 executedFrom, 411 pathsToOpen, 412 timeout, 413 env 414 }); 415 } else if ( 416 (pathsToOpen && pathsToOpen.length > 0) || 417 (foldersToOpen && foldersToOpen.length > 0) 418 ) { 419 return this.openPaths({ 420 pathsToOpen, 421 foldersToOpen, 422 executedFrom, 423 pidToKillWhenClosed, 424 newWindow, 425 devMode, 426 safeMode, 427 profileStartup, 428 clearWindowState, 429 addToLastWindow, 430 env 431 }); 432 } else if (urlsToOpen && urlsToOpen.length > 0) { 433 return Promise.all( 434 urlsToOpen.map(urlToOpen => 435 this.openUrl({ urlToOpen, devMode, safeMode, env }) 436 ) 437 ); 438 } else { 439 // Always open an editor window if this is the first instance of Atom. 440 return this.openPath({ 441 pathToOpen: null, 442 pidToKillWhenClosed, 443 newWindow, 444 devMode, 445 safeMode, 446 profileStartup, 447 clearWindowState, 448 addToLastWindow, 449 env 450 }); 451 } 452 } 453 454 // Public: Create a new {AtomWindow} bound to this application. 455 createWindow(settings) { 456 return new AtomWindow(this, this.fileRecoveryService, settings); 457 } 458 459 // Public: Removes the {AtomWindow} from the global window list. 460 removeWindow(window) { 461 this.windowStack.removeWindow(window); 462 if (this.getAllWindows().length === 0) { 463 if (this.applicationMenu != null) { 464 this.applicationMenu.enableWindowSpecificItems(false); 465 } 466 if (['win32', 'linux'].includes(process.platform)) { 467 app.quit(); 468 return; 469 } 470 } 471 if (!window.isSpec) this.saveCurrentWindowOptions(true); 472 } 473 474 // Public: Adds the {AtomWindow} to the global window list. 475 addWindow(window) { 476 this.windowStack.addWindow(window); 477 if (this.applicationMenu) 478 this.applicationMenu.addWindow(window.browserWindow); 479 480 window.once('window:loaded', () => { 481 this.autoUpdateManager && 482 this.autoUpdateManager.emitUpdateAvailableEvent(window); 483 }); 484 485 if (!window.isSpec) { 486 const focusHandler = () => this.windowStack.touch(window); 487 const blurHandler = () => this.saveCurrentWindowOptions(false); 488 window.browserWindow.on('focus', focusHandler); 489 window.browserWindow.on('blur', blurHandler); 490 window.browserWindow.once('closed', () => { 491 this.windowStack.removeWindow(window); 492 window.browserWindow.removeListener('focus', focusHandler); 493 window.browserWindow.removeListener('blur', blurHandler); 494 }); 495 window.browserWindow.webContents.once('did-finish-load', blurHandler); 496 this.saveCurrentWindowOptions(false); 497 } 498 } 499 500 getAllWindows() { 501 return this.windowStack.all().slice(); 502 } 503 504 getLastFocusedWindow(predicate) { 505 return this.windowStack.getLastFocusedWindow(predicate); 506 } 507 508 // Creates server to listen for additional atom application launches. 509 // 510 // You can run the atom command multiple times, but after the first launch 511 // the other launches will just pass their information to this server and then 512 // close immediately. 513 async listenForArgumentsFromNewProcess() { 514 this.socketSecretPromise = createSocketSecret(this.version); 515 this.socketSecret = await this.socketSecretPromise; 516 this.socketPath = getSocketPath(this.socketSecret); 517 518 await this.deleteSocketFile(); 519 520 const server = net.createServer(connection => { 521 let data = ''; 522 connection.on('data', chunk => { 523 data += chunk; 524 }); 525 connection.on('end', () => { 526 try { 527 const options = decryptOptions(data, this.socketSecret); 528 this.openWithOptions(options); 529 } catch (e) { 530 // Error while parsing/decrypting the options passed by the client. 531 // We cannot trust the client, aborting. 532 } 533 }); 534 }); 535 536 return new Promise(resolve => { 537 server.listen(this.socketPath, resolve); 538 server.on('error', error => 539 console.error('Application server failed', error) 540 ); 541 }); 542 } 543 544 async deleteSocketFile() { 545 if (process.platform === 'win32') return; 546 547 if (!this.socketSecretPromise) { 548 return; 549 } 550 await this.socketSecretPromise; 551 552 if (fs.existsSync(this.socketPath)) { 553 try { 554 fs.unlinkSync(this.socketPath); 555 } catch (error) { 556 // Ignore ENOENT errors in case the file was deleted between the exists 557 // check and the call to unlink sync. This occurred occasionally on CI 558 // which is why this check is here. 559 if (error.code !== 'ENOENT') throw error; 560 } 561 } 562 } 563 564 async deleteSocketSecretFile() { 565 if (!this.socketSecretPromise) { 566 return; 567 } 568 await this.socketSecretPromise; 569 570 const socketSecretPath = getSocketSecretPath(this.version); 571 572 if (fs.existsSync(socketSecretPath)) { 573 try { 574 fs.unlinkSync(socketSecretPath); 575 } catch (error) { 576 // Ignore ENOENT errors in case the file was deleted between the exists 577 // check and the call to unlink sync. 578 if (error.code !== 'ENOENT') throw error; 579 } 580 } 581 } 582 583 // Registers basic application commands, non-idempotent. 584 handleEvents() { 585 const createOpenSettings = ({ event, sameWindow }) => { 586 const targetWindow = event 587 ? this.atomWindowForEvent(event) 588 : this.focusedWindow(); 589 return { 590 devMode: targetWindow ? targetWindow.devMode : false, 591 safeMode: targetWindow ? targetWindow.safeMode : false, 592 window: sameWindow && targetWindow ? targetWindow : null 593 }; 594 }; 595 596 this.on('application:quit', () => app.quit()); 597 this.on('application:new-window', () => 598 this.openPath(createOpenSettings({})) 599 ); 600 this.on('application:new-file', () => 601 (this.focusedWindow() || this).openPath() 602 ); 603 this.on('application:open-dev', () => 604 this.promptForPathToOpen('all', { devMode: true }) 605 ); 606 this.on('application:open-safe', () => 607 this.promptForPathToOpen('all', { safeMode: true }) 608 ); 609 this.on('application:inspect', ({ x, y, atomWindow }) => { 610 if (!atomWindow) atomWindow = this.focusedWindow(); 611 if (atomWindow) atomWindow.browserWindow.inspectElement(x, y); 612 }); 613 614 this.on('application:open-documentation', () => 615 shell.openExternal('http://flight-manual.atom.io') 616 ); 617 this.on('application:open-discussions', () => 618 shell.openExternal('https://discuss.atom.io') 619 ); 620 this.on('application:open-faq', () => 621 shell.openExternal('https://atom.io/faq') 622 ); 623 this.on('application:open-terms-of-use', () => 624 shell.openExternal('https://atom.io/terms') 625 ); 626 this.on('application:report-issue', () => 627 shell.openExternal( 628 'https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs' 629 ) 630 ); 631 this.on('application:search-issues', () => 632 shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom') 633 ); 634 635 this.on('application:install-update', () => { 636 this.quitting = true; 637 this.quittingForUpdate = true; 638 this.autoUpdateManager.install(); 639 }); 640 641 this.on('application:check-for-update', () => 642 this.autoUpdateManager.check() 643 ); 644 645 if (process.platform === 'darwin') { 646 this.on('application:reopen-project', ({ paths }) => { 647 this.openPaths({ pathsToOpen: paths }); 648 }); 649 650 this.on('application:open', () => { 651 this.promptForPathToOpen( 652 'all', 653 createOpenSettings({ sameWindow: true }), 654 getDefaultPath() 655 ); 656 }); 657 this.on('application:open-file', () => { 658 this.promptForPathToOpen( 659 'file', 660 createOpenSettings({ sameWindow: true }), 661 getDefaultPath() 662 ); 663 }); 664 this.on('application:open-folder', () => { 665 this.promptForPathToOpen( 666 'folder', 667 createOpenSettings({ sameWindow: true }), 668 getDefaultPath() 669 ); 670 }); 671 672 this.on('application:bring-all-windows-to-front', () => 673 Menu.sendActionToFirstResponder('arrangeInFront:') 674 ); 675 this.on('application:hide', () => 676 Menu.sendActionToFirstResponder('hide:') 677 ); 678 this.on('application:hide-other-applications', () => 679 Menu.sendActionToFirstResponder('hideOtherApplications:') 680 ); 681 this.on('application:minimize', () => 682 Menu.sendActionToFirstResponder('performMiniaturize:') 683 ); 684 this.on('application:unhide-all-applications', () => 685 Menu.sendActionToFirstResponder('unhideAllApplications:') 686 ); 687 this.on('application:zoom', () => 688 Menu.sendActionToFirstResponder('zoom:') 689 ); 690 } else { 691 this.on('application:minimize', () => { 692 const window = this.focusedWindow(); 693 if (window) window.minimize(); 694 }); 695 this.on('application:zoom', function() { 696 const window = this.focusedWindow(); 697 if (window) window.maximize(); 698 }); 699 } 700 701 this.openPathOnEvent('application:about', 'atom://about'); 702 this.openPathOnEvent('application:show-settings', 'atom://config'); 703 this.openPathOnEvent('application:open-your-config', 'atom://.atom/config'); 704 this.openPathOnEvent( 705 'application:open-your-init-script', 706 'atom://.atom/init-script' 707 ); 708 this.openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap'); 709 this.openPathOnEvent( 710 'application:open-your-snippets', 711 'atom://.atom/snippets' 712 ); 713 this.openPathOnEvent( 714 'application:open-your-stylesheet', 715 'atom://.atom/stylesheet' 716 ); 717 this.openPathOnEvent( 718 'application:open-license', 719 path.join(process.resourcesPath, 'LICENSE.md') 720 ); 721 722 this.configFile.onDidChange(settings => { 723 for (let window of this.getAllWindows()) { 724 window.didChangeUserSettings(settings); 725 } 726 this.config.resetUserSettings(settings); 727 }); 728 729 this.configFile.onDidError(message => { 730 const window = this.focusedWindow() || this.getLastFocusedWindow(); 731 if (window) { 732 window.didFailToReadUserSettings(message); 733 } else { 734 console.error(message); 735 } 736 }); 737 738 this.disposable.add( 739 ipcHelpers.on(app, 'before-quit', async event => { 740 let resolveBeforeQuitPromise; 741 this.lastBeforeQuitPromise = new Promise(resolve => { 742 resolveBeforeQuitPromise = resolve; 743 }); 744 745 if (!this.quitting) { 746 this.quitting = true; 747 event.preventDefault(); 748 const windowUnloadPromises = this.getAllWindows().map( 749 async window => { 750 const unloaded = await window.prepareToUnload(); 751 if (unloaded) { 752 window.close(); 753 await window.closedPromise; 754 } 755 return unloaded; 756 } 757 ); 758 const windowUnloadedResults = await Promise.all(windowUnloadPromises); 759 if (windowUnloadedResults.every(Boolean)) { 760 app.quit(); 761 } else { 762 this.quitting = false; 763 } 764 } 765 766 resolveBeforeQuitPromise(); 767 }) 768 ); 769 770 this.disposable.add( 771 ipcHelpers.on(app, 'will-quit', () => { 772 this.killAllProcesses(); 773 774 return Promise.all([ 775 this.deleteSocketFile(), 776 this.deleteSocketSecretFile() 777 ]); 778 }) 779 ); 780 781 // Triggered by the 'open-file' event from Electron: 782 // https://electronjs.org/docs/api/app#event-open-file-macos 783 // For example, this is fired when a file is dragged and dropped onto the Atom application icon in the dock. 784 this.disposable.add( 785 ipcHelpers.on(app, 'open-file', (event, pathToOpen) => { 786 event.preventDefault(); 787 this.openPath({ pathToOpen }); 788 }) 789 ); 790 791 this.disposable.add( 792 ipcHelpers.on(app, 'open-url', (event, urlToOpen) => { 793 event.preventDefault(); 794 this.openUrl({ 795 urlToOpen, 796 devMode: this.devMode, 797 safeMode: this.safeMode 798 }); 799 }) 800 ); 801 802 this.disposable.add( 803 ipcHelpers.on(app, 'activate', (event, hasVisibleWindows) => { 804 if (hasVisibleWindows) return; 805 if (event) event.preventDefault(); 806 this.emit('application:new-window'); 807 }) 808 ); 809 810 this.disposable.add( 811 ipcHelpers.on(ipcMain, 'restart-application', () => { 812 this.restart(); 813 }) 814 ); 815 816 this.disposable.add( 817 ipcHelpers.on(ipcMain, 'resolve-proxy', (event, requestId, url) => { 818 event.sender.session.resolveProxy(url, proxy => { 819 if (!event.sender.isDestroyed()) 820 event.sender.send('did-resolve-proxy', requestId, proxy); 821 }); 822 }) 823 ); 824 825 this.disposable.add( 826 ipcHelpers.on(ipcMain, 'did-change-history-manager', event => { 827 for (let atomWindow of this.getAllWindows()) { 828 const { webContents } = atomWindow.browserWindow; 829 if (webContents !== event.sender) 830 webContents.send('did-change-history-manager'); 831 } 832 }) 833 ); 834 835 // A request from the associated render process to open a set of paths using the standard window location logic. 836 // Used for application:reopen-project. 837 this.disposable.add( 838 ipcHelpers.on(ipcMain, 'open', (event, options) => { 839 if (options) { 840 if (typeof options.pathsToOpen === 'string') { 841 options.pathsToOpen = [options.pathsToOpen]; 842 } 843 844 if (options.here) { 845 options.window = this.atomWindowForEvent(event); 846 } 847 848 if (options.pathsToOpen && options.pathsToOpen.length > 0) { 849 this.openPaths(options); 850 } else { 851 this.addWindow(this.createWindow(options)); 852 } 853 } else { 854 this.promptForPathToOpen('all', {}); 855 } 856 }) 857 ); 858 859 // Prompt for a file, folder, or either, then open the chosen paths. Files will be opened in the originating 860 // window; folders will be opened in a new window unless an existing window exactly contains all of them. 861 this.disposable.add( 862 ipcHelpers.on(ipcMain, 'open-chosen-any', (event, defaultPath) => { 863 this.promptForPathToOpen( 864 'all', 865 createOpenSettings({ event, sameWindow: true }), 866 defaultPath 867 ); 868 }) 869 ); 870 this.disposable.add( 871 ipcHelpers.on(ipcMain, 'open-chosen-file', (event, defaultPath) => { 872 this.promptForPathToOpen( 873 'file', 874 createOpenSettings({ event, sameWindow: true }), 875 defaultPath 876 ); 877 }) 878 ); 879 this.disposable.add( 880 ipcHelpers.on(ipcMain, 'open-chosen-folder', (event, defaultPath) => { 881 this.promptForPathToOpen( 882 'folder', 883 createOpenSettings({ event }), 884 defaultPath 885 ); 886 }) 887 ); 888 889 this.disposable.add( 890 ipcHelpers.on( 891 ipcMain, 892 'update-application-menu', 893 (event, template, menu) => { 894 const window = BrowserWindow.fromWebContents(event.sender); 895 if (this.applicationMenu) 896 this.applicationMenu.update(window, template, menu); 897 } 898 ) 899 ); 900 901 this.disposable.add( 902 ipcHelpers.on( 903 ipcMain, 904 'run-package-specs', 905 (event, packageSpecPath, options = {}) => { 906 this.runTests( 907 Object.assign( 908 { 909 resourcePath: this.devResourcePath, 910 pathsToOpen: [packageSpecPath], 911 headless: false 912 }, 913 options 914 ) 915 ); 916 } 917 ) 918 ); 919 920 this.disposable.add( 921 ipcHelpers.on(ipcMain, 'run-benchmarks', (event, benchmarksPath) => { 922 this.runBenchmarks({ 923 resourcePath: this.devResourcePath, 924 pathsToOpen: [benchmarksPath], 925 headless: false, 926 test: false 927 }); 928 }) 929 ); 930 931 this.disposable.add( 932 ipcHelpers.on(ipcMain, 'command', (event, command) => { 933 this.emit(command); 934 }) 935 ); 936 937 this.disposable.add( 938 ipcHelpers.on(ipcMain, 'window-command', (event, command, ...args) => { 939 const window = BrowserWindow.fromWebContents(event.sender); 940 return window && window.emit(command, ...args); 941 }) 942 ); 943 944 this.disposable.add( 945 ipcHelpers.respondTo( 946 'window-method', 947 (browserWindow, method, ...args) => { 948 const window = this.atomWindowForBrowserWindow(browserWindow); 949 if (window) window[method](...args); 950 } 951 ) 952 ); 953 954 this.disposable.add( 955 ipcHelpers.on(ipcMain, 'pick-folder', (event, responseChannel) => { 956 this.promptForPath('folder', paths => 957 event.sender.send(responseChannel, paths) 958 ); 959 }) 960 ); 961 962 this.disposable.add( 963 ipcHelpers.respondTo('set-window-size', (window, width, height) => { 964 window.setSize(width, height); 965 }) 966 ); 967 968 this.disposable.add( 969 ipcHelpers.respondTo('set-window-position', (window, x, y) => { 970 window.setPosition(x, y); 971 }) 972 ); 973 974 this.disposable.add( 975 ipcHelpers.respondTo( 976 'set-user-settings', 977 (window, settings, filePath) => { 978 if (!this.quitting) { 979 return ConfigFile.at(filePath || this.configFilePath).update( 980 JSON.parse(settings) 981 ); 982 } 983 } 984 ) 985 ); 986 987 this.disposable.add( 988 ipcHelpers.respondTo('center-window', window => window.center()) 989 ); 990 this.disposable.add( 991 ipcHelpers.respondTo('focus-window', window => window.focus()) 992 ); 993 this.disposable.add( 994 ipcHelpers.respondTo('show-window', window => window.show()) 995 ); 996 this.disposable.add( 997 ipcHelpers.respondTo('hide-window', window => window.hide()) 998 ); 999 this.disposable.add( 1000 ipcHelpers.respondTo( 1001 'get-temporary-window-state', 1002 window => window.temporaryState 1003 ) 1004 ); 1005 1006 this.disposable.add( 1007 ipcHelpers.respondTo('set-temporary-window-state', (win, state) => { 1008 win.temporaryState = state; 1009 }) 1010 ); 1011 1012 this.disposable.add( 1013 ipcHelpers.on( 1014 ipcMain, 1015 'write-text-to-selection-clipboard', 1016 (event, text) => clipboard.writeText(text, 'selection') 1017 ) 1018 ); 1019 1020 this.disposable.add( 1021 ipcHelpers.on(ipcMain, 'write-to-stdout', (event, output) => 1022 process.stdout.write(output) 1023 ) 1024 ); 1025 1026 this.disposable.add( 1027 ipcHelpers.on(ipcMain, 'write-to-stderr', (event, output) => 1028 process.stderr.write(output) 1029 ) 1030 ); 1031 1032 this.disposable.add( 1033 ipcHelpers.on(ipcMain, 'add-recent-document', (event, filename) => 1034 app.addRecentDocument(filename) 1035 ) 1036 ); 1037 1038 this.disposable.add( 1039 ipcHelpers.on( 1040 ipcMain, 1041 'execute-javascript-in-dev-tools', 1042 (event, code) => 1043 event.sender.devToolsWebContents && 1044 event.sender.devToolsWebContents.executeJavaScript(code) 1045 ) 1046 ); 1047 1048 this.disposable.add( 1049 ipcHelpers.on(ipcMain, 'get-auto-update-manager-state', event => { 1050 event.returnValue = this.autoUpdateManager.getState(); 1051 }) 1052 ); 1053 1054 this.disposable.add( 1055 ipcHelpers.on(ipcMain, 'get-auto-update-manager-error', event => { 1056 event.returnValue = this.autoUpdateManager.getErrorMessage(); 1057 }) 1058 ); 1059 1060 this.disposable.add( 1061 ipcHelpers.respondTo('will-save-path', (window, path) => 1062 this.fileRecoveryService.willSavePath(window, path) 1063 ) 1064 ); 1065 1066 this.disposable.add( 1067 ipcHelpers.respondTo('did-save-path', (window, path) => 1068 this.fileRecoveryService.didSavePath(window, path) 1069 ) 1070 ); 1071 1072 this.disposable.add(this.disableZoomOnDisplayChange()); 1073 } 1074 1075 setupDockMenu() { 1076 if (process.platform === 'darwin') { 1077 return app.dock.setMenu( 1078 Menu.buildFromTemplate([ 1079 { 1080 label: 'New Window', 1081 click: () => this.emit('application:new-window') 1082 } 1083 ]) 1084 ); 1085 } 1086 } 1087 1088 initializeAtomHome(configDirPath) { 1089 if (!fs.existsSync(configDirPath)) { 1090 const templateConfigDirPath = fs.resolve(this.resourcePath, 'dot-atom'); 1091 fs.copySync(templateConfigDirPath, configDirPath); 1092 } 1093 } 1094 1095 // Public: Executes the given command. 1096 // 1097 // If it isn't handled globally, delegate to the currently focused window. 1098 // 1099 // command - The string representing the command. 1100 // args - The optional arguments to pass along. 1101 sendCommand(command, ...args) { 1102 if (!this.emit(command, ...args)) { 1103 const focusedWindow = this.focusedWindow(); 1104 if (focusedWindow) { 1105 return focusedWindow.sendCommand(command, ...args); 1106 } else { 1107 return this.sendCommandToFirstResponder(command); 1108 } 1109 } 1110 } 1111 1112 // Public: Executes the given command on the given window. 1113 // 1114 // command - The string representing the command. 1115 // atomWindow - The {AtomWindow} to send the command to. 1116 // args - The optional arguments to pass along. 1117 sendCommandToWindow(command, atomWindow, ...args) { 1118 if (!this.emit(command, ...args)) { 1119 if (atomWindow) { 1120 return atomWindow.sendCommand(command, ...args); 1121 } else { 1122 return this.sendCommandToFirstResponder(command); 1123 } 1124 } 1125 } 1126 1127 // Translates the command into macOS action and sends it to application's first 1128 // responder. 1129 sendCommandToFirstResponder(command) { 1130 if (process.platform !== 'darwin') return false; 1131 1132 switch (command) { 1133 case 'core:undo': 1134 Menu.sendActionToFirstResponder('undo:'); 1135 break; 1136 case 'core:redo': 1137 Menu.sendActionToFirstResponder('redo:'); 1138 break; 1139 case 'core:copy': 1140 Menu.sendActionToFirstResponder('copy:'); 1141 break; 1142 case 'core:cut': 1143 Menu.sendActionToFirstResponder('cut:'); 1144 break; 1145 case 'core:paste': 1146 Menu.sendActionToFirstResponder('paste:'); 1147 break; 1148 case 'core:select-all': 1149 Menu.sendActionToFirstResponder('selectAll:'); 1150 break; 1151 default: 1152 return false; 1153 } 1154 return true; 1155 } 1156 1157 // Public: Open the given path in the focused window when the event is 1158 // triggered. 1159 // 1160 // A new window will be created if there is no currently focused window. 1161 // 1162 // eventName - The event to listen for. 1163 // pathToOpen - The path to open when the event is triggered. 1164 openPathOnEvent(eventName, pathToOpen) { 1165 this.on(eventName, () => { 1166 const window = this.focusedWindow(); 1167 if (window) { 1168 return window.openPath(pathToOpen); 1169 } else { 1170 return this.openPath({ pathToOpen }); 1171 } 1172 }); 1173 } 1174 1175 // Returns the {AtomWindow} for the given locations. 1176 windowForLocations(locationsToOpen, devMode, safeMode) { 1177 return this.getLastFocusedWindow( 1178 window => 1179 !window.isSpec && 1180 window.devMode === devMode && 1181 window.safeMode === safeMode && 1182 window.containsLocations(locationsToOpen) 1183 ); 1184 } 1185 1186 // Returns the {AtomWindow} for the given ipcMain event. 1187 atomWindowForEvent({ sender }) { 1188 return this.atomWindowForBrowserWindow( 1189 BrowserWindow.fromWebContents(sender) 1190 ); 1191 } 1192 1193 atomWindowForBrowserWindow(browserWindow) { 1194 return this.getAllWindows().find( 1195 atomWindow => atomWindow.browserWindow === browserWindow 1196 ); 1197 } 1198 1199 // Public: Returns the currently focused {AtomWindow} or undefined if none. 1200 focusedWindow() { 1201 return this.getAllWindows().find(window => window.isFocused()); 1202 } 1203 1204 // Get the platform-specific window offset for new windows. 1205 getWindowOffsetForCurrentPlatform() { 1206 const offsetByPlatform = { 1207 darwin: 22, 1208 win32: 26 1209 }; 1210 return offsetByPlatform[process.platform] || 0; 1211 } 1212 1213 // Get the dimensions for opening a new window by cascading as appropriate to 1214 // the platform. 1215 getDimensionsForNewWindow() { 1216 const window = this.focusedWindow() || this.getLastFocusedWindow(); 1217 if (!window || window.isMaximized()) return; 1218 const dimensions = window.getDimensions(); 1219 if (dimensions) { 1220 const offset = this.getWindowOffsetForCurrentPlatform(); 1221 dimensions.x += offset; 1222 dimensions.y += offset; 1223 return dimensions; 1224 } 1225 } 1226 1227 // Public: Opens a single path, in an existing window if possible. 1228 // 1229 // options - 1230 // :pathToOpen - The file path to open 1231 // :pidToKillWhenClosed - The integer of the pid to kill 1232 // :newWindow - Boolean of whether this should be opened in a new window. 1233 // :devMode - Boolean to control the opened window's dev mode. 1234 // :safeMode - Boolean to control the opened window's safe mode. 1235 // :profileStartup - Boolean to control creating a profile of the startup time. 1236 // :window - {AtomWindow} to open file paths in. 1237 // :addToLastWindow - Boolean of whether this should be opened in last focused window. 1238 openPath({ 1239 pathToOpen, 1240 pidToKillWhenClosed, 1241 newWindow, 1242 devMode, 1243 safeMode, 1244 profileStartup, 1245 window, 1246 clearWindowState, 1247 addToLastWindow, 1248 env 1249 } = {}) { 1250 return this.openPaths({ 1251 pathsToOpen: [pathToOpen], 1252 pidToKillWhenClosed, 1253 newWindow, 1254 devMode, 1255 safeMode, 1256 profileStartup, 1257 window, 1258 clearWindowState, 1259 addToLastWindow, 1260 env 1261 }); 1262 } 1263 1264 // Public: Opens multiple paths, in existing windows if possible. 1265 // 1266 // options - 1267 // :pathsToOpen - The array of file paths to open 1268 // :foldersToOpen - An array of additional paths to open that must be existing directories 1269 // :pidToKillWhenClosed - The integer of the pid to kill 1270 // :newWindow - Boolean of whether this should be opened in a new window. 1271 // :devMode - Boolean to control the opened window's dev mode. 1272 // :safeMode - Boolean to control the opened window's safe mode. 1273 // :windowDimensions - Object with height and width keys. 1274 // :window - {AtomWindow} to open file paths in. 1275 // :addToLastWindow - Boolean of whether this should be opened in last focused window. 1276 async openPaths({ 1277 pathsToOpen, 1278 foldersToOpen, 1279 executedFrom, 1280 pidToKillWhenClosed, 1281 newWindow, 1282 devMode, 1283 safeMode, 1284 windowDimensions, 1285 profileStartup, 1286 window, 1287 clearWindowState, 1288 addToLastWindow, 1289 env 1290 } = {}) { 1291 if (!env) env = process.env; 1292 if (!pathsToOpen) pathsToOpen = []; 1293 if (!foldersToOpen) foldersToOpen = []; 1294 1295 devMode = Boolean(devMode); 1296 safeMode = Boolean(safeMode); 1297 clearWindowState = Boolean(clearWindowState); 1298 1299 const locationsToOpen = await Promise.all( 1300 pathsToOpen.map(pathToOpen => 1301 this.parsePathToOpen(pathToOpen, executedFrom, { 1302 hasWaitSession: pidToKillWhenClosed != null 1303 }) 1304 ) 1305 ); 1306 1307 for (const folderToOpen of foldersToOpen) { 1308 locationsToOpen.push({ 1309 pathToOpen: folderToOpen, 1310 initialLine: null, 1311 initialColumn: null, 1312 exists: true, 1313 isDirectory: true, 1314 hasWaitSession: pidToKillWhenClosed != null 1315 }); 1316 } 1317 1318 if (locationsToOpen.length === 0) { 1319 return; 1320 } 1321 1322 const hasNonEmptyPath = locationsToOpen.some( 1323 location => location.pathToOpen 1324 ); 1325 const createNewWindow = newWindow || !hasNonEmptyPath; 1326 1327 let existingWindow; 1328 1329 if (!createNewWindow) { 1330 // An explicitly provided AtomWindow has precedence. 1331 existingWindow = window; 1332 1333 // If no window is specified and at least one path is provided, locate an existing window that contains all 1334 // provided paths. 1335 if (!existingWindow && hasNonEmptyPath) { 1336 existingWindow = this.windowForLocations( 1337 locationsToOpen, 1338 devMode, 1339 safeMode 1340 ); 1341 } 1342 1343 // No window specified, no existing window found, and addition to the last window requested. Find the last 1344 // focused window that matches the requested dev and safe modes. 1345 if (!existingWindow && addToLastWindow) { 1346 existingWindow = this.getLastFocusedWindow(win => { 1347 return ( 1348 !win.isSpec && win.devMode === devMode && win.safeMode === safeMode 1349 ); 1350 }); 1351 } 1352 1353 // Fall back to the last focused window that has no project roots. 1354 if (!existingWindow) { 1355 existingWindow = this.getLastFocusedWindow( 1356 win => !win.isSpec && !win.hasProjectPaths() 1357 ); 1358 } 1359 1360 // One last case: if *no* paths are directories, add to the last focused window. 1361 if (!existingWindow) { 1362 const noDirectories = locationsToOpen.every( 1363 location => !location.isDirectory 1364 ); 1365 if (noDirectories) { 1366 existingWindow = this.getLastFocusedWindow(win => { 1367 return ( 1368 !win.isSpec && 1369 win.devMode === devMode && 1370 win.safeMode === safeMode 1371 ); 1372 }); 1373 } 1374 } 1375 } 1376 1377 let openedWindow; 1378 if (existingWindow) { 1379 openedWindow = existingWindow; 1380 StartupTime.addMarker('main-process:atom-application:open-in-existing'); 1381 openedWindow.openLocations(locationsToOpen); 1382 if (openedWindow.isMinimized()) { 1383 openedWindow.restore(); 1384 } else { 1385 openedWindow.focus(); 1386 } 1387 openedWindow.replaceEnvironment(env); 1388 } else { 1389 let resourcePath, windowInitializationScript; 1390 if (devMode) { 1391 try { 1392 windowInitializationScript = require.resolve( 1393 path.join( 1394 this.devResourcePath, 1395 'src', 1396 'initialize-application-window' 1397 ) 1398 ); 1399 resourcePath = this.devResourcePath; 1400 } catch (error) {} 1401 } 1402 1403 if (!windowInitializationScript) { 1404 windowInitializationScript = require.resolve( 1405 '../initialize-application-window' 1406 ); 1407 } 1408 if (!resourcePath) resourcePath = this.resourcePath; 1409 if (!windowDimensions) 1410 windowDimensions = this.getDimensionsForNewWindow(); 1411 1412 StartupTime.addMarker('main-process:atom-application:create-window'); 1413 openedWindow = this.createWindow({ 1414 locationsToOpen, 1415 windowInitializationScript, 1416 resourcePath, 1417 devMode, 1418 safeMode, 1419 windowDimensions, 1420 profileStartup, 1421 clearWindowState, 1422 env 1423 }); 1424 this.addWindow(openedWindow); 1425 openedWindow.focus(); 1426 } 1427 1428 if (pidToKillWhenClosed != null) { 1429 if (!this.waitSessionsByWindow.has(openedWindow)) { 1430 this.waitSessionsByWindow.set(openedWindow, []); 1431 } 1432 this.waitSessionsByWindow.get(openedWindow).push({ 1433 pid: pidToKillWhenClosed, 1434 remainingPaths: new Set( 1435 locationsToOpen.map(location => location.pathToOpen).filter(Boolean) 1436 ) 1437 }); 1438 } 1439 1440 openedWindow.browserWindow.once('closed', () => 1441 this.killProcessesForWindow(openedWindow) 1442 ); 1443 return openedWindow; 1444 } 1445 1446 // Kill all processes associated with opened windows. 1447 killAllProcesses() { 1448 for (let window of this.waitSessionsByWindow.keys()) { 1449 this.killProcessesForWindow(window); 1450 } 1451 } 1452 1453 killProcessesForWindow(window) { 1454 const sessions = this.waitSessionsByWindow.get(window); 1455 if (!sessions) return; 1456 for (const session of sessions) { 1457 this.killProcess(session.pid); 1458 } 1459 this.waitSessionsByWindow.delete(window); 1460 } 1461 1462 windowDidClosePathWithWaitSession(window, initialPath) { 1463 const waitSessions = this.waitSessionsByWindow.get(window); 1464 if (!waitSessions) return; 1465 for (let i = waitSessions.length - 1; i >= 0; i--) { 1466 const session = waitSessions[i]; 1467 session.remainingPaths.delete(initialPath); 1468 if (session.remainingPaths.size === 0) { 1469 this.killProcess(session.pid); 1470 waitSessions.splice(i, 1); 1471 } 1472 } 1473 } 1474 1475 // Kill the process with the given pid. 1476 killProcess(pid) { 1477 try { 1478 const parsedPid = parseInt(pid); 1479 if (isFinite(parsedPid)) this._killProcess(parsedPid); 1480 } catch (error) { 1481 if (error.code !== 'ESRCH') { 1482 console.log( 1483 `Killing process ${pid} failed: ${ 1484 error.code != null ? error.code : error.message 1485 }` 1486 ); 1487 } 1488 } 1489 } 1490 1491 async saveCurrentWindowOptions(allowEmpty = false) { 1492 if (this.quitting) return; 1493 1494 const state = { 1495 version: APPLICATION_STATE_VERSION, 1496 windows: this.getAllWindows() 1497 .filter(window => !window.isSpec) 1498 .map(window => ({ projectRoots: window.projectRoots })) 1499 }; 1500 state.windows.reverse(); 1501 1502 if (state.windows.length > 0 || allowEmpty) { 1503 await this.storageFolder.store('application.json', state); 1504 this.emit('application:did-save-state'); 1505 } 1506 } 1507 1508 async loadPreviousWindowOptions() { 1509 const state = await this.storageFolder.load('application.json'); 1510 if (!state) { 1511 return []; 1512 } 1513 1514 if (state.version === APPLICATION_STATE_VERSION) { 1515 // Atom >=1.36.1 1516 // Schema: {version: '1', windows: [{projectRoots: ['<root-dir>', ...]}, ...]} 1517 return state.windows.map(each => ({ 1518 foldersToOpen: each.projectRoots, 1519 devMode: this.devMode, 1520 safeMode: this.safeMode, 1521 newWindow: true 1522 })); 1523 } else if (state.version === undefined) { 1524 // Atom <= 1.36.0 1525 // Schema: [{initialPaths: ['<root-dir>', ...]}, ...] 1526 return Promise.all( 1527 state.map(async windowState => { 1528 // Classify each window's initialPaths as directories or non-directories 1529 const classifiedPaths = await Promise.all( 1530 windowState.initialPaths.map( 1531 initialPath => 1532 new Promise(resolve => { 1533 fs.isDirectory(initialPath, isDir => 1534 resolve({ initialPath, isDir }) 1535 ); 1536 }) 1537 ) 1538 ); 1539 1540 // Only accept initialPaths that are existing directories 1541 return { 1542 foldersToOpen: classifiedPaths 1543 .filter(({ isDir }) => isDir) 1544 .map(({ initialPath }) => initialPath), 1545 devMode: this.devMode, 1546 safeMode: this.safeMode, 1547 newWindow: true 1548 }; 1549 }) 1550 ); 1551 } else { 1552 // Unrecognized version (from a newer Atom?) 1553 return []; 1554 } 1555 } 1556 1557 // Open an atom:// url. 1558 // 1559 // The host of the URL being opened is assumed to be the package name 1560 // responsible for opening the URL. A new window will be created with 1561 // that package's `urlMain` as the bootstrap script. 1562 // 1563 // options - 1564 // :urlToOpen - The atom:// url to open. 1565 // :devMode - Boolean to control the opened window's dev mode. 1566 // :safeMode - Boolean to control the opened window's safe mode. 1567 openUrl({ urlToOpen, devMode, safeMode, env }) { 1568 const parsedUrl = url.parse(urlToOpen, true); 1569 if (parsedUrl.protocol !== 'atom:') return; 1570 1571 const pack = this.findPackageWithName(parsedUrl.host, devMode); 1572 if (pack && pack.urlMain) { 1573 return this.openPackageUrlMain( 1574 parsedUrl.host, 1575 pack.urlMain, 1576 urlToOpen, 1577 devMode, 1578 safeMode, 1579 env 1580 ); 1581 } else { 1582 return this.openPackageUriHandler( 1583 urlToOpen, 1584 parsedUrl, 1585 devMode, 1586 safeMode, 1587 env 1588 ); 1589 } 1590 } 1591 1592 openPackageUriHandler(url, parsedUrl, devMode, safeMode, env) { 1593 let bestWindow; 1594 1595 if (parsedUrl.host === 'core') { 1596 const predicate = require('../core-uri-handlers').windowPredicate( 1597 parsedUrl 1598 ); 1599 bestWindow = this.getLastFocusedWindow( 1600 win => !win.isSpecWindow() && predicate(win) 1601 ); 1602 } 1603 1604 if (!bestWindow) 1605 bestWindow = this.getLastFocusedWindow(win => !win.isSpecWindow()); 1606 1607 if (bestWindow) { 1608 bestWindow.sendURIMessage(url); 1609 bestWindow.focus(); 1610 return bestWindow; 1611 } else { 1612 let windowInitializationScript; 1613 let { resourcePath } = this; 1614 if (devMode) { 1615 try { 1616 windowInitializationScript = require.resolve( 1617 path.join( 1618 this.devResourcePath, 1619 'src', 1620 'initialize-application-window' 1621 ) 1622 ); 1623 resourcePath = this.devResourcePath; 1624 } catch (error) {} 1625 } 1626 1627 if (!windowInitializationScript) { 1628 windowInitializationScript = require.resolve( 1629 '../initialize-application-window' 1630 ); 1631 } 1632 1633 const windowDimensions = this.getDimensionsForNewWindow(); 1634 const window = this.createWindow({ 1635 resourcePath, 1636 windowInitializationScript, 1637 devMode, 1638 safeMode, 1639 windowDimensions, 1640 env 1641 }); 1642 this.addWindow(window); 1643 window.on('window:loaded', () => window.sendURIMessage(url)); 1644 return window; 1645 } 1646 } 1647 1648 findPackageWithName(packageName, devMode) { 1649 return this.getPackageManager(devMode) 1650 .getAvailablePackageMetadata() 1651 .find(({ name }) => name === packageName); 1652 } 1653 1654 openPackageUrlMain( 1655 packageName, 1656 packageUrlMain, 1657 urlToOpen, 1658 devMode, 1659 safeMode, 1660 env 1661 ) { 1662 const packagePath = this.getPackageManager(devMode).resolvePackagePath( 1663 packageName 1664 ); 1665 const windowInitializationScript = path.resolve( 1666 packagePath, 1667 packageUrlMain 1668 ); 1669 const windowDimensions = this.getDimensionsForNewWindow(); 1670 const window = this.createWindow({ 1671 windowInitializationScript, 1672 resourcePath: this.resourcePath, 1673 devMode, 1674 safeMode, 1675 urlToOpen, 1676 windowDimensions, 1677 env 1678 }); 1679 this.addWindow(window); 1680 return window; 1681 } 1682 1683 getPackageManager(devMode) { 1684 if (this.packages == null) { 1685 const PackageManager = require('../package-manager'); 1686 this.packages = new PackageManager({}); 1687 this.packages.initialize({ 1688 configDirPath: process.env.ATOM_HOME, 1689 devMode, 1690 resourcePath: this.resourcePath 1691 }); 1692 } 1693 1694 return this.packages; 1695 } 1696 1697 // Opens up a new {AtomWindow} to run specs within. 1698 // 1699 // options - 1700 // :headless - A Boolean that, if true, will close the window upon 1701 // completion. 1702 // :resourcePath - The path to include specs from. 1703 // :specPath - The directory to load specs from. 1704 // :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages 1705 // and ~/.atom/dev/packages, defaults to false. 1706 runTests({ 1707 headless, 1708 resourcePath, 1709 executedFrom, 1710 pathsToOpen, 1711 logFile, 1712 safeMode, 1713 timeout, 1714 env 1715 }) { 1716 let windowInitializationScript; 1717 if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) { 1718 ({ resourcePath } = this); 1719 } 1720 1721 const timeoutInSeconds = Number.parseFloat(timeout); 1722 if (!Number.isNaN(timeoutInSeconds)) { 1723 const timeoutHandler = function() { 1724 console.log( 1725 `The test suite has timed out because it has been running for more than ${timeoutInSeconds} seconds.` 1726 ); 1727 return process.exit(124); // Use the same exit code as the UNIX timeout util. 1728 }; 1729 setTimeout(timeoutHandler, timeoutInSeconds * 1000); 1730 } 1731 1732 try { 1733 windowInitializationScript = require.resolve( 1734 path.resolve(this.devResourcePath, 'src', 'initialize-test-window') 1735 ); 1736 } catch (error) { 1737 windowInitializationScript = require.resolve( 1738 path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window') 1739 ); 1740 } 1741 1742 const testPaths = []; 1743 if (pathsToOpen != null) { 1744 for (let pathToOpen of pathsToOpen) { 1745 testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen))); 1746 } 1747 } 1748 1749 if (testPaths.length === 0) { 1750 process.stderr.write('Error: Specify at least one test path\n\n'); 1751 process.exit(1); 1752 } 1753 1754 const legacyTestRunnerPath = this.resolveLegacyTestRunnerPath(); 1755 const testRunnerPath = this.resolveTestRunnerPath(testPaths[0]); 1756 const devMode = true; 1757 const isSpec = true; 1758 if (safeMode == null) { 1759 safeMode = false; 1760 } 1761 const window = this.createWindow({ 1762 windowInitializationScript, 1763 resourcePath, 1764 headless, 1765 isSpec, 1766 devMode, 1767 testRunnerPath, 1768 legacyTestRunnerPath, 1769 testPaths, 1770 logFile, 1771 safeMode, 1772 env 1773 }); 1774 this.addWindow(window); 1775 if (env) window.replaceEnvironment(env); 1776 return window; 1777 } 1778 1779 runBenchmarks({ 1780 headless, 1781 test, 1782 resourcePath, 1783 executedFrom, 1784 pathsToOpen, 1785 env 1786 }) { 1787 let windowInitializationScript; 1788 if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) { 1789 ({ resourcePath } = this); 1790 } 1791 1792 try { 1793 windowInitializationScript = require.resolve( 1794 path.resolve(this.devResourcePath, 'src', 'initialize-benchmark-window') 1795 ); 1796 } catch (error) { 1797 windowInitializationScript = require.resolve( 1798 path.resolve( 1799 __dirname, 1800 '..', 1801 '..', 1802 'src', 1803 'initialize-benchmark-window' 1804 ) 1805 ); 1806 } 1807 1808 const benchmarkPaths = []; 1809 if (pathsToOpen != null) { 1810 for (let pathToOpen of pathsToOpen) { 1811 benchmarkPaths.push( 1812 path.resolve(executedFrom, fs.normalize(pathToOpen)) 1813 ); 1814 } 1815 } 1816 1817 if (benchmarkPaths.length === 0) { 1818 process.stderr.write('Error: Specify at least one benchmark path.\n\n'); 1819 process.exit(1); 1820 } 1821 1822 const devMode = true; 1823 const isSpec = true; 1824 const safeMode = false; 1825 const window = this.createWindow({ 1826 windowInitializationScript, 1827 resourcePath, 1828 headless, 1829 test, 1830 isSpec, 1831 devMode, 1832 benchmarkPaths, 1833 safeMode, 1834 env 1835 }); 1836 this.addWindow(window); 1837 return window; 1838 } 1839 1840 resolveTestRunnerPath(testPath) { 1841 let packageRoot; 1842 if (FindParentDir == null) { 1843 FindParentDir = require('find-parent-dir'); 1844 } 1845 1846 if ((packageRoot = FindParentDir.sync(testPath, 'package.json'))) { 1847 const packageMetadata = require(path.join(packageRoot, 'package.json')); 1848 if (packageMetadata.atomTestRunner) { 1849 let testRunnerPath; 1850 if (Resolve == null) { 1851 Resolve = require('resolve'); 1852 } 1853 if ( 1854 (testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, { 1855 basedir: packageRoot, 1856 extensions: Object.keys(require.extensions) 1857 })) 1858 ) { 1859 return testRunnerPath; 1860 } else { 1861 process.stderr.write( 1862 `Error: Could not resolve test runner path '${ 1863 packageMetadata.atomTestRunner 1864 }'` 1865 ); 1866 process.exit(1); 1867 } 1868 } 1869 } 1870 1871 return this.resolveLegacyTestRunnerPath(); 1872 } 1873 1874 resolveLegacyTestRunnerPath() { 1875 try { 1876 return require.resolve( 1877 path.resolve(this.devResourcePath, 'spec', 'jasmine-test-runner') 1878 ); 1879 } catch (error) { 1880 return require.resolve( 1881 path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner') 1882 ); 1883 } 1884 } 1885 1886 async parsePathToOpen(pathToOpen, executedFrom, extra) { 1887 const result = Object.assign( 1888 { 1889 pathToOpen, 1890 initialColumn: null, 1891 initialLine: null, 1892 exists: false, 1893 isDirectory: false, 1894 isFile: false 1895 }, 1896 extra 1897 ); 1898 1899 if (!pathToOpen) { 1900 return result; 1901 } 1902 1903 result.pathToOpen = result.pathToOpen.replace(/[:\s]+$/, ''); 1904 const match = result.pathToOpen.match(LocationSuffixRegExp); 1905 1906 if (match != null) { 1907 result.pathToOpen = result.pathToOpen.slice(0, -match[0].length); 1908 if (match[1]) { 1909 result.initialLine = Math.max(0, parseInt(match[1].slice(1), 10) - 1); 1910 } 1911 if (match[2]) { 1912 result.initialColumn = Math.max(0, parseInt(match[2].slice(1), 10) - 1); 1913 } 1914 } 1915 1916 const normalizedPath = path.normalize( 1917 path.resolve(executedFrom, fs.normalize(result.pathToOpen)) 1918 ); 1919 if (!url.parse(pathToOpen).protocol) { 1920 result.pathToOpen = normalizedPath; 1921 } 1922 1923 await new Promise((resolve, reject) => { 1924 fs.stat(result.pathToOpen, (err, st) => { 1925 if (err) { 1926 if (err.code === 'ENOENT' || err.code === 'EACCES') { 1927 result.exists = false; 1928 resolve(); 1929 } else { 1930 reject(err); 1931 } 1932 return; 1933 } 1934 1935 result.exists = true; 1936 result.isFile = st.isFile(); 1937 result.isDirectory = st.isDirectory(); 1938 resolve(); 1939 }); 1940 }); 1941 1942 return result; 1943 } 1944 1945 // Opens a native dialog to prompt the user for a path. 1946 // 1947 // Once paths are selected, they're opened in a new or existing {AtomWindow}s. 1948 // 1949 // options - 1950 // :type - A String which specifies the type of the dialog, could be 'file', 1951 // 'folder' or 'all'. The 'all' is only available on macOS. 1952 // :devMode - A Boolean which controls whether any newly opened windows 1953 // should be in dev mode or not. 1954 // :safeMode - A Boolean which controls whether any newly opened windows 1955 // should be in safe mode or not. 1956 // :window - An {AtomWindow} to use for opening selected file paths as long as 1957 // all are files. 1958 // :path - An optional String which controls the default path to which the 1959 // file dialog opens. 1960 promptForPathToOpen(type, { devMode, safeMode, window }, path = null) { 1961 return this.promptForPath( 1962 type, 1963 async pathsToOpen => { 1964 let targetWindow; 1965 1966 // Open in :window as long as no chosen paths are folders. If any chosen path is a folder, open in a 1967 // new window instead. 1968 if (type === 'folder') { 1969 targetWindow = null; 1970 } else if (type === 'file') { 1971 targetWindow = window; 1972 } else if (type === 'all') { 1973 const areDirectories = await Promise.all( 1974 pathsToOpen.map( 1975 pathToOpen => 1976 new Promise(resolve => fs.isDirectory(pathToOpen, resolve)) 1977 ) 1978 ); 1979 if (!areDirectories.some(Boolean)) { 1980 targetWindow = window; 1981 } 1982 } 1983 1984 return this.openPaths({ 1985 pathsToOpen, 1986 devMode, 1987 safeMode, 1988 window: targetWindow 1989 }); 1990 }, 1991 path 1992 ); 1993 } 1994 1995 promptForPath(type, callback, path) { 1996 const properties = (() => { 1997 switch (type) { 1998 case 'file': 1999 return ['openFile']; 2000 case 'folder': 2001 return ['openDirectory']; 2002 case 'all': 2003 return ['openFile', 'openDirectory']; 2004 default: 2005 throw new Error(`${type} is an invalid type for promptForPath`); 2006 } 2007 })(); 2008 2009 // Show the open dialog as child window on Windows and Linux, and as an independent dialog on macOS. This matches 2010 // most native apps. 2011 const parentWindow = 2012 process.platform === 'darwin' ? null : BrowserWindow.getFocusedWindow(); 2013 2014 const openOptions = { 2015 properties: properties.concat(['multiSelections', 'createDirectory']), 2016 title: (() => { 2017 switch (type) { 2018 case 'file': 2019 return 'Open File'; 2020 case 'folder': 2021 return 'Open Folder'; 2022 default: 2023 return 'Open'; 2024 } 2025 })() 2026 }; 2027 2028 // File dialog defaults to project directory of currently active editor 2029 if (path) openOptions.defaultPath = path; 2030 dialog.showOpenDialog(parentWindow, openOptions, callback); 2031 } 2032 2033 promptForRestart() { 2034 dialog.showMessageBox( 2035 BrowserWindow.getFocusedWindow(), 2036 { 2037 type: 'warning', 2038 title: 'Restart required', 2039 message: 2040 'You will need to restart Atom for this change to take effect.', 2041 buttons: ['Restart Atom', 'Cancel'] 2042 }, 2043 response => { 2044 if (response === 0) this.restart(); 2045 } 2046 ); 2047 } 2048 2049 restart() { 2050 const args = []; 2051 if (this.safeMode) args.push('--safe'); 2052 if (this.logFile != null) args.push(`--log-file=${this.logFile}`); 2053 if (this.userDataDir != null) 2054 args.push(`--user-data-dir=${this.userDataDir}`); 2055 if (this.devMode) { 2056 args.push('--dev'); 2057 args.push(`--resource-path=${this.resourcePath}`); 2058 } 2059 app.relaunch({ args }); 2060 app.quit(); 2061 } 2062 2063 disableZoomOnDisplayChange() { 2064 const callback = () => { 2065 this.getAllWindows().map(window => window.disableZoom()); 2066 }; 2067 2068 // Set the limits every time a display is added or removed, otherwise the 2069 // configuration gets reset to the default, which allows zooming the 2070 // webframe. 2071 screen.on('display-added', callback); 2072 screen.on('display-removed', callback); 2073 return new Disposable(() => { 2074 screen.removeListener('display-added', callback); 2075 screen.removeListener('display-removed', callback); 2076 }); 2077 } 2078 }; 2079 2080 class WindowStack { 2081 constructor(windows = []) { 2082 this.addWindow = this.addWindow.bind(this); 2083 this.touch = this.touch.bind(this); 2084 this.removeWindow = this.removeWindow.bind(this); 2085 this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this); 2086 this.all = this.all.bind(this); 2087 this.windows = windows; 2088 } 2089 2090 addWindow(window) { 2091 this.removeWindow(window); 2092 return this.windows.unshift(window); 2093 } 2094 2095 touch(window) { 2096 return this.addWindow(window); 2097 } 2098 2099 removeWindow(window) { 2100 const currentIndex = this.windows.indexOf(window); 2101 if (currentIndex > -1) { 2102 return this.windows.splice(currentIndex, 1); 2103 } 2104 } 2105 2106 getLastFocusedWindow(predicate) { 2107 if (predicate == null) { 2108 predicate = win => true; 2109 } 2110 return this.windows.find(predicate); 2111 } 2112 2113 all() { 2114 return this.windows; 2115 } 2116 }