/ src / main-process / atom-application.js
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  }