Files
atom/src/main-process/atom-application.js
Jordan Eldredge 878629dd0a Avoid sending async updates on config update
In
7a5d727e22
the 'set-user-settings' callback was changed so that it nolonger
returned the promise created by `ConfigFile.update()`. This meant that
the logic in
[`ApplicateDelegate.onDidChangeUserSetting()`](https://github.com/atom/atom/blob/5f0231b/src/application-delegate.js#L193-L206) which uses
`.pendingSettingUpdateCount` to avoid triggering in response to calls to
ApplicationDelegate.setUserSettings` was no longer effective.

I noticed this when updating settings via the setting view. If you type
at just the right (wrong?) cadence, you'll notice that your input can
get updated with stale values.

Something like this:

```
You type            "a"      "b"            "c"
config.set          "a"      "ab"           "ac"
config.observe                        "a"         "ab"    "ac"
Input value         "a"      "ab"     "a"   "ac"  "ab"    "ac"
```

It's possible that is the same as https://github.com/atom/settings-view/issues/1062

After this change typing in settings input seems to behave as expected.
2018-12-05 14:34:30 -08:00

1426 lines
47 KiB
JavaScript

const AtomWindow = require('./atom-window')
const ApplicationMenu = require('./application-menu')
const AtomProtocolHandler = require('./atom-protocol-handler')
const AutoUpdateManager = require('./auto-update-manager')
const StorageFolder = require('../storage-folder')
const Config = require('../config')
const ConfigFile = require('../config-file')
const FileRecoveryService = require('./file-recovery-service')
const ipcHelpers = require('../ipc-helpers')
const {BrowserWindow, Menu, app, clipboard, dialog, ipcMain, shell, screen} = require('electron')
const {CompositeDisposable, Disposable} = require('event-kit')
const crypto = require('crypto')
const fs = require('fs-plus')
const path = require('path')
const os = require('os')
const net = require('net')
const url = require('url')
const {EventEmitter} = require('events')
const _ = require('underscore-plus')
let FindParentDir = null
let Resolve = null
const ConfigSchema = require('../config-schema')
const LocationSuffixRegExp = /(:\d+)(:\d+)?$/
// The application's singleton class.
//
// It's the entry point into the Atom application and maintains the global state
// of the application.
//
module.exports =
class AtomApplication extends EventEmitter {
// Public: The entry point into the Atom application.
static open (options) {
if (!options.socketPath) {
const {username} = os.userInfo()
// Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets
// on case-insensitive filesystems due to arbitrary case differences in paths.
const atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase()
const hash = crypto
.createHash('sha1')
.update(options.version)
.update('|')
.update(process.arch)
.update('|')
.update(username || '')
.update('|')
.update(atomHomeUnique)
// We only keep the first 12 characters of the hash as not to have excessively long
// socket file. Note that macOS/BSD limit the length of socket file paths (see #15081).
// The replace calls convert the digest into "URL and Filename Safe" encoding (see RFC 4648).
const atomInstanceDigest = hash
.digest('base64')
.substring(0, 12)
.replace(/\+/g, '-')
.replace(/\//g, '_')
if (process.platform === 'win32') {
options.socketPath = `\\\\.\\pipe\\atom-${atomInstanceDigest}-sock`
} else {
options.socketPath = path.join(os.tmpdir(), `atom-${atomInstanceDigest}.sock`)
}
}
// FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely
// take a few seconds to trigger 'error' event, it could be a bug of node
// or electron, before it's fixed we check the existence of socketPath to
// speedup startup.
if ((process.platform !== 'win32' && !fs.existsSync(options.socketPath)) ||
options.test || options.benchmark || options.benchmarkTest) {
new AtomApplication(options).initialize(options)
return
}
const client = net.connect({path: options.socketPath}, () => {
client.write(JSON.stringify(options), () => {
client.end()
app.quit()
})
})
client.on('error', () => new AtomApplication(options).initialize(options))
}
exit (status) {
app.exit(status)
}
constructor (options) {
super()
this.quitting = false
this.getAllWindows = this.getAllWindows.bind(this)
this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this)
this.resourcePath = options.resourcePath
this.devResourcePath = options.devResourcePath
this.version = options.version
this.devMode = options.devMode
this.safeMode = options.safeMode
this.socketPath = options.socketPath
this.logFile = options.logFile
this.userDataDir = options.userDataDir
this._killProcess = options.killProcess || process.kill.bind(process)
if (options.test || options.benchmark || options.benchmarkTest) this.socketPath = null
this.waitSessionsByWindow = new Map()
this.windowStack = new WindowStack()
this.initializeAtomHome(process.env.ATOM_HOME)
const configFilePath = fs.existsSync(path.join(process.env.ATOM_HOME, 'config.json'))
? path.join(process.env.ATOM_HOME, 'config.json')
: path.join(process.env.ATOM_HOME, 'config.cson')
this.configFile = ConfigFile.at(configFilePath)
this.config = new Config({
saveCallback: settings => {
if (!this.quitting) {
return this.configFile.update(settings)
}
}
})
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
this.fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, 'recovery'))
this.storageFolder = new StorageFolder(process.env.ATOM_HOME)
this.autoUpdateManager = new AutoUpdateManager(
this.version,
options.test || options.benchmark || options.benchmarkTest,
this.config
)
this.disposable = new CompositeDisposable()
this.handleEvents()
}
// This stuff was previously done in the constructor, but we want to be able to construct this object
// for testing purposes without booting up the world. As you add tests, feel free to move instantiation
// of these various sub-objects into the constructor, but you'll need to remove the side-effects they
// perform during their construction, adding an initialize method that you call here.
async initialize (options) {
global.atomApplication = this
// DEPRECATED: This can be removed at some point (added in 1.13)
// It converts `useCustomTitleBar: true` to `titleBar: "custom"`
if (process.platform === 'darwin' && this.config.get('core.useCustomTitleBar')) {
this.config.unset('core.useCustomTitleBar')
this.config.set('core.titleBar', 'custom')
}
this.applicationMenu = new ApplicationMenu(this.version, this.autoUpdateManager)
this.atomProtocolHandler = new AtomProtocolHandler(this.resourcePath, this.safeMode)
this.listenForArgumentsFromNewProcess()
this.setupDockMenu()
const result = await this.launch(options)
this.autoUpdateManager.initialize()
return result
}
async destroy () {
const windowsClosePromises = this.getAllWindows().map(window => {
window.close()
return window.closedPromise
})
await Promise.all(windowsClosePromises)
this.disposable.dispose()
}
async launch (options) {
if (!this.configFilePromise) {
this.configFilePromise = this.configFile.watch()
this.disposable.add(await this.configFilePromise)
this.config.onDidChange('core.titleBar', () => this.promptForRestart())
this.config.onDidChange('core.colorProfile', () => this.promptForRestart())
}
const optionsForWindowsToOpen = []
let shouldReopenPreviousWindows = false
if (options.test || options.benchmark || options.benchmarkTest) {
optionsForWindowsToOpen.push(options)
} else if ((options.pathsToOpen && options.pathsToOpen.length > 0) ||
(options.urlsToOpen && options.urlsToOpen.length > 0)) {
optionsForWindowsToOpen.push(options)
shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') === 'always'
} else {
shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') !== 'no'
}
if (shouldReopenPreviousWindows) {
for (const previousOptions of await this.loadPreviousWindowOptions()) {
optionsForWindowsToOpen.push(Object.assign({}, options, previousOptions))
}
}
if (optionsForWindowsToOpen.length === 0) {
optionsForWindowsToOpen.push(options)
}
return optionsForWindowsToOpen.map(options => this.openWithOptions(options))
}
openWithOptions (options) {
const {
initialPaths,
pathsToOpen,
executedFrom,
urlsToOpen,
benchmark,
benchmarkTest,
test,
pidToKillWhenClosed,
devMode,
safeMode,
newWindow,
logFile,
profileStartup,
timeout,
clearWindowState,
addToLastWindow,
env
} = options
app.focus()
if (test) {
return this.runTests({
headless: true,
devMode,
resourcePath: this.resourcePath,
executedFrom,
pathsToOpen,
logFile,
timeout,
env
})
} else if (benchmark || benchmarkTest) {
return this.runBenchmarks({
headless: true,
test: benchmarkTest,
resourcePath: this.resourcePath,
executedFrom,
pathsToOpen,
timeout,
env
})
} else if (pathsToOpen.length > 0) {
return this.openPaths({
initialPaths,
pathsToOpen,
executedFrom,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
clearWindowState,
addToLastWindow,
env
})
} else if (urlsToOpen.length > 0) {
return urlsToOpen.map(urlToOpen => this.openUrl({urlToOpen, devMode, safeMode, env}))
} else {
// Always open a editor window if this is the first instance of Atom.
return this.openPath({
initialPaths,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
clearWindowState,
addToLastWindow,
env
})
}
}
// Public: Removes the {AtomWindow} from the global window list.
removeWindow (window) {
this.windowStack.removeWindow(window)
if (this.getAllWindows().length === 0) {
if (this.applicationMenu != null) {
this.applicationMenu.enableWindowSpecificItems(false)
}
if (['win32', 'linux'].includes(process.platform)) {
app.quit()
return
}
}
if (!window.isSpec) this.saveCurrentWindowOptions(true)
}
// Public: Adds the {AtomWindow} to the global window list.
addWindow (window) {
this.windowStack.addWindow(window)
if (this.applicationMenu) this.applicationMenu.addWindow(window.browserWindow)
window.once('window:loaded', () => {
this.autoUpdateManager && this.autoUpdateManager.emitUpdateAvailableEvent(window)
})
if (!window.isSpec) {
const focusHandler = () => this.windowStack.touch(window)
const blurHandler = () => this.saveCurrentWindowOptions(false)
window.browserWindow.on('focus', focusHandler)
window.browserWindow.on('blur', blurHandler)
window.browserWindow.once('closed', () => {
this.windowStack.removeWindow(window)
window.browserWindow.removeListener('focus', focusHandler)
window.browserWindow.removeListener('blur', blurHandler)
})
window.browserWindow.webContents.once('did-finish-load', blurHandler)
}
}
getAllWindows () {
return this.windowStack.all().slice()
}
getLastFocusedWindow (predicate) {
return this.windowStack.getLastFocusedWindow(predicate)
}
// Creates server to listen for additional atom application launches.
//
// You can run the atom command multiple times, but after the first launch
// the other launches will just pass their information to this server and then
// close immediately.
listenForArgumentsFromNewProcess () {
if (!this.socketPath) return
this.deleteSocketFile()
const server = net.createServer(connection => {
let data = ''
connection.on('data', chunk => { data += chunk })
connection.on('end', () => this.openWithOptions(JSON.parse(data)))
})
server.listen(this.socketPath)
server.on('error', error => console.error('Application server failed', error))
}
deleteSocketFile () {
if (process.platform === 'win32' || !this.socketPath) return
if (fs.existsSync(this.socketPath)) {
try {
fs.unlinkSync(this.socketPath)
} catch (error) {
// Ignore ENOENT errors in case the file was deleted between the exists
// check and the call to unlink sync. This occurred occasionally on CI
// which is why this check is here.
if (error.code !== 'ENOENT') throw error
}
}
}
// Registers basic application commands, non-idempotent.
handleEvents () {
const getLoadSettings = () => {
const window = this.focusedWindow()
return {devMode: window && window.devMode, safeMode: window && window.safeMode}
}
this.on('application:quit', () => app.quit())
this.on('application:new-window', () => this.openPath(getLoadSettings()))
this.on('application:new-file', () => (this.focusedWindow() || this).openPath())
this.on('application:open-dev', () => this.promptForPathToOpen('all', {devMode: true}))
this.on('application:open-safe', () => this.promptForPathToOpen('all', {safeMode: true}))
this.on('application:inspect', ({x, y, atomWindow}) => {
if (!atomWindow) atomWindow = this.focusedWindow()
if (atomWindow) atomWindow.browserWindow.inspectElement(x, y)
})
this.on('application:open-documentation', () => shell.openExternal('http://flight-manual.atom.io'))
this.on('application:open-discussions', () => shell.openExternal('https://discuss.atom.io'))
this.on('application:open-faq', () => shell.openExternal('https://atom.io/faq'))
this.on('application:open-terms-of-use', () => shell.openExternal('https://atom.io/terms'))
this.on('application:report-issue', () => shell.openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs'))
this.on('application:search-issues', () => shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom'))
this.on('application:install-update', () => {
this.quitting = true
this.autoUpdateManager.install()
})
this.on('application:check-for-update', () => this.autoUpdateManager.check())
if (process.platform === 'darwin') {
this.on('application:bring-all-windows-to-front', () => Menu.sendActionToFirstResponder('arrangeInFront:'))
this.on('application:hide', () => Menu.sendActionToFirstResponder('hide:'))
this.on('application:hide-other-applications', () => Menu.sendActionToFirstResponder('hideOtherApplications:'))
this.on('application:minimize', () => Menu.sendActionToFirstResponder('performMiniaturize:'))
this.on('application:unhide-all-applications', () => Menu.sendActionToFirstResponder('unhideAllApplications:'))
this.on('application:zoom', () => Menu.sendActionToFirstResponder('zoom:'))
} else {
this.on('application:minimize', () => {
const window = this.focusedWindow()
if (window) window.minimize()
})
this.on('application:zoom', function () {
const window = this.focusedWindow()
if (window) window.maximize()
})
}
this.openPathOnEvent('application:about', 'atom://about')
this.openPathOnEvent('application:show-settings', 'atom://config')
this.openPathOnEvent('application:open-your-config', 'atom://.atom/config')
this.openPathOnEvent('application:open-your-init-script', 'atom://.atom/init-script')
this.openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap')
this.openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets')
this.openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet')
this.openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
this.configFile.onDidChange(settings => {
for (let window of this.getAllWindows()) {
window.didChangeUserSettings(settings)
}
this.config.resetUserSettings(settings)
})
this.configFile.onDidError(message => {
const window = this.focusedWindow() || this.getLastFocusedWindow()
if (window) window.didFailToReadUserSettings(message)
})
this.disposable.add(ipcHelpers.on(app, 'before-quit', async event => {
let resolveBeforeQuitPromise
this.lastBeforeQuitPromise = new Promise(resolve => { resolveBeforeQuitPromise = resolve })
if (!this.quitting) {
this.quitting = true
event.preventDefault()
const windowUnloadPromises = this.getAllWindows().map(async window => {
const unloaded = await window.prepareToUnload()
if (unloaded) {
window.close()
await window.closedPromise
}
return unloaded
})
const windowUnloadedResults = await Promise.all(windowUnloadPromises)
if (windowUnloadedResults.every(Boolean)) {
app.quit()
} else {
this.quitting = false
}
}
resolveBeforeQuitPromise()
}))
this.disposable.add(ipcHelpers.on(app, 'will-quit', () => {
this.killAllProcesses()
this.deleteSocketFile()
}))
this.disposable.add(ipcHelpers.on(app, 'open-file', (event, pathToOpen) => {
event.preventDefault()
this.openPath({pathToOpen})
}))
this.disposable.add(ipcHelpers.on(app, 'open-url', (event, urlToOpen) => {
event.preventDefault()
this.openUrl({urlToOpen, devMode: this.devMode, safeMode: this.safeMode})
}))
this.disposable.add(ipcHelpers.on(app, 'activate', (event, hasVisibleWindows) => {
if (hasVisibleWindows) return
if (event) event.preventDefault()
this.emit('application:new-window')
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'restart-application', () => {
this.restart()
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'resolve-proxy', (event, requestId, url) => {
event.sender.session.resolveProxy(url, proxy => {
if (!event.sender.isDestroyed()) event.sender.send('did-resolve-proxy', requestId, proxy)
})
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'did-change-history-manager', event => {
for (let atomWindow of this.getAllWindows()) {
const {webContents} = atomWindow.browserWindow
if (webContents !== event.sender) webContents.send('did-change-history-manager')
}
}))
// A request from the associated render process to open a new render process.
this.disposable.add(ipcHelpers.on(ipcMain, 'open', (event, options) => {
const window = this.atomWindowForEvent(event)
if (options) {
if (typeof options.pathsToOpen === 'string') {
options.pathsToOpen = [options.pathsToOpen]
}
if (options.pathsToOpen && options.pathsToOpen.length > 0) {
options.window = window
this.openPaths(options)
} else {
this.addWindow(new AtomWindow(this, this.fileRecoveryService, options))
}
} else {
this.promptForPathToOpen('all', {window})
}
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'update-application-menu', (event, template, menu) => {
const window = BrowserWindow.fromWebContents(event.sender)
if (this.applicationMenu) this.applicationMenu.update(window, template, menu)
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'run-package-specs', (event, packageSpecPath, options = {}) => {
this.runTests(Object.assign({
resourcePath: this.devResourcePath,
pathsToOpen: [packageSpecPath],
headless: false
}, options))
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'run-benchmarks', (event, benchmarksPath) => {
this.runBenchmarks({
resourcePath: this.devResourcePath,
pathsToOpen: [benchmarksPath],
headless: false,
test: false
})
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'command', (event, command) => {
this.emit(command)
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'open-command', (event, command, defaultPath) => {
switch (command) {
case 'application:open':
return this.promptForPathToOpen('all', getLoadSettings(), defaultPath)
case 'application:open-file':
return this.promptForPathToOpen('file', getLoadSettings(), defaultPath)
case 'application:open-folder':
return this.promptForPathToOpen('folder', getLoadSettings(), defaultPath)
default:
return console.log(`Invalid open-command received: ${command}`)
}
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'window-command', (event, command, ...args) => {
const window = BrowserWindow.fromWebContents(event.sender)
return window.emit(command, ...args)
}))
this.disposable.add(ipcHelpers.respondTo('window-method', (browserWindow, method, ...args) => {
const window = this.atomWindowForBrowserWindow(browserWindow)
if (window) window[method](...args)
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'pick-folder', (event, responseChannel) => {
this.promptForPath('folder', paths => event.sender.send(responseChannel, paths))
}))
this.disposable.add(ipcHelpers.respondTo('set-window-size', (window, width, height) => {
window.setSize(width, height)
}))
this.disposable.add(ipcHelpers.respondTo('set-window-position', (window, x, y) => {
window.setPosition(x, y)
}))
this.disposable.add(ipcHelpers.respondTo('set-user-settings', (window, settings, filePath) => {
if (!this.quitting) {
return ConfigFile.at(filePath || this.configFilePath).update(JSON.parse(settings))
}
}))
this.disposable.add(ipcHelpers.respondTo('center-window', window => window.center()))
this.disposable.add(ipcHelpers.respondTo('focus-window', window => window.focus()))
this.disposable.add(ipcHelpers.respondTo('show-window', window => window.show()))
this.disposable.add(ipcHelpers.respondTo('hide-window', window => window.hide()))
this.disposable.add(ipcHelpers.respondTo('get-temporary-window-state', window => window.temporaryState))
this.disposable.add(ipcHelpers.respondTo('set-temporary-window-state', (win, state) => {
win.temporaryState = state
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'write-text-to-selection-clipboard', (event, text) =>
clipboard.writeText(text, 'selection')
))
this.disposable.add(ipcHelpers.on(ipcMain, 'write-to-stdout', (event, output) =>
process.stdout.write(output)
))
this.disposable.add(ipcHelpers.on(ipcMain, 'write-to-stderr', (event, output) =>
process.stderr.write(output)
))
this.disposable.add(ipcHelpers.on(ipcMain, 'add-recent-document', (event, filename) =>
app.addRecentDocument(filename)
))
this.disposable.add(ipcHelpers.on(ipcMain, 'execute-javascript-in-dev-tools', (event, code) =>
event.sender.devToolsWebContents && event.sender.devToolsWebContents.executeJavaScript(code)
))
this.disposable.add(ipcHelpers.on(ipcMain, 'get-auto-update-manager-state', event => {
event.returnValue = this.autoUpdateManager.getState()
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'get-auto-update-manager-error', event => {
event.returnValue = this.autoUpdateManager.getErrorMessage()
}))
this.disposable.add(ipcHelpers.respondTo('will-save-path', (window, path) =>
this.fileRecoveryService.willSavePath(window, path)
))
this.disposable.add(ipcHelpers.respondTo('did-save-path', (window, path) =>
this.fileRecoveryService.didSavePath(window, path)
))
this.disposable.add(ipcHelpers.on(ipcMain, 'did-change-paths', () =>
this.saveCurrentWindowOptions(false)
))
this.disposable.add(this.disableZoomOnDisplayChange())
}
setupDockMenu () {
if (process.platform === 'darwin') {
return app.dock.setMenu(Menu.buildFromTemplate([
{label: 'New Window', click: () => this.emit('application:new-window')}
]))
}
}
initializeAtomHome (configDirPath) {
if (!fs.existsSync(configDirPath)) {
const templateConfigDirPath = fs.resolve(this.resourcePath, 'dot-atom')
fs.copySync(templateConfigDirPath, configDirPath)
}
}
// Public: Executes the given command.
//
// If it isn't handled globally, delegate to the currently focused window.
//
// command - The string representing the command.
// args - The optional arguments to pass along.
sendCommand (command, ...args) {
if (!this.emit(command, ...args)) {
const focusedWindow = this.focusedWindow()
if (focusedWindow) {
return focusedWindow.sendCommand(command, ...args)
} else {
return this.sendCommandToFirstResponder(command)
}
}
}
// Public: Executes the given command on the given window.
//
// command - The string representing the command.
// atomWindow - The {AtomWindow} to send the command to.
// args - The optional arguments to pass along.
sendCommandToWindow (command, atomWindow, ...args) {
if (!this.emit(command, ...args)) {
if (atomWindow) {
return atomWindow.sendCommand(command, ...args)
} else {
return this.sendCommandToFirstResponder(command)
}
}
}
// Translates the command into macOS action and sends it to application's first
// responder.
sendCommandToFirstResponder (command) {
if (process.platform !== 'darwin') return false
switch (command) {
case 'core:undo':
Menu.sendActionToFirstResponder('undo:')
break
case 'core:redo':
Menu.sendActionToFirstResponder('redo:')
break
case 'core:copy':
Menu.sendActionToFirstResponder('copy:')
break
case 'core:cut':
Menu.sendActionToFirstResponder('cut:')
break
case 'core:paste':
Menu.sendActionToFirstResponder('paste:')
break
case 'core:select-all':
Menu.sendActionToFirstResponder('selectAll:')
break
default:
return false
}
return true
}
// Public: Open the given path in the focused window when the event is
// triggered.
//
// A new window will be created if there is no currently focused window.
//
// eventName - The event to listen for.
// pathToOpen - The path to open when the event is triggered.
openPathOnEvent (eventName, pathToOpen) {
this.on(eventName, () => {
const window = this.focusedWindow()
if (window) {
return window.openPath(pathToOpen)
} else {
return this.openPath({pathToOpen})
}
})
}
// Returns the {AtomWindow} for the given paths.
windowForPaths (pathsToOpen, devMode) {
return this.getAllWindows().find(window =>
window.devMode === devMode && window.containsPaths(pathsToOpen)
)
}
// Returns the {AtomWindow} for the given ipcMain event.
atomWindowForEvent ({sender}) {
return this.atomWindowForBrowserWindow(BrowserWindow.fromWebContents(sender))
}
atomWindowForBrowserWindow (browserWindow) {
return this.getAllWindows().find(atomWindow => atomWindow.browserWindow === browserWindow)
}
// Public: Returns the currently focused {AtomWindow} or undefined if none.
focusedWindow () {
return this.getAllWindows().find(window => window.isFocused())
}
// Get the platform-specific window offset for new windows.
getWindowOffsetForCurrentPlatform () {
const offsetByPlatform = {
darwin: 22,
win32: 26
}
return offsetByPlatform[process.platform] || 0
}
// Get the dimensions for opening a new window by cascading as appropriate to
// the platform.
getDimensionsForNewWindow () {
const window = this.focusedWindow() || this.getLastFocusedWindow()
if (!window || window.isMaximized()) return
const dimensions = window.getDimensions()
if (dimensions) {
const offset = this.getWindowOffsetForCurrentPlatform()
dimensions.x += offset
dimensions.y += offset
return dimensions
}
}
// Public: Opens a single path, in an existing window if possible.
//
// options -
// :pathToOpen - The file path to open
// :pidToKillWhenClosed - The integer of the pid to kill
// :newWindow - Boolean of whether this should be opened in a new window.
// :devMode - Boolean to control the opened window's dev mode.
// :safeMode - Boolean to control the opened window's safe mode.
// :profileStartup - Boolean to control creating a profile of the startup time.
// :window - {AtomWindow} to open file paths in.
// :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPath ({
initialPaths,
pathToOpen,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
window,
clearWindowState,
addToLastWindow,
env
} = {}) {
return this.openPaths({
initialPaths,
pathsToOpen: [pathToOpen],
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
window,
clearWindowState,
addToLastWindow,
env
})
}
// Public: Opens multiple paths, in existing windows if possible.
//
// options -
// :pathsToOpen - The array of file paths to open
// :pidToKillWhenClosed - The integer of the pid to kill
// :newWindow - Boolean of whether this should be opened in a new window.
// :devMode - Boolean to control the opened window's dev mode.
// :safeMode - Boolean to control the opened window's safe mode.
// :windowDimensions - Object with height and width keys.
// :window - {AtomWindow} to open file paths in.
// :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPaths ({
initialPaths,
pathsToOpen,
executedFrom,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
windowDimensions,
profileStartup,
window,
clearWindowState,
addToLastWindow,
env
} = {}) {
if (!pathsToOpen || pathsToOpen.length === 0) return
if (!env) env = process.env
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
clearWindowState = Boolean(clearWindowState)
const locationsToOpen = []
for (let i = 0; i < pathsToOpen.length; i++) {
const location = this.parsePathToOpen(pathsToOpen[i], executedFrom, addToLastWindow)
location.forceAddToWindow = addToLastWindow
location.hasWaitSession = pidToKillWhenClosed != null
locationsToOpen.push(location)
pathsToOpen[i] = location.pathToOpen
}
let existingWindow
if (!newWindow) {
existingWindow = this.windowForPaths(pathsToOpen, devMode)
if (!existingWindow) {
let lastWindow = window || this.getLastFocusedWindow()
if (lastWindow && lastWindow.devMode === devMode) {
if (addToLastWindow || (
locationsToOpen.every(({stat}) => stat && stat.isFile()) ||
(locationsToOpen.some(({stat}) => stat && stat.isDirectory()) && !lastWindow.hasProjectPath()))) {
existingWindow = lastWindow
}
}
}
}
let openedWindow
if (existingWindow) {
openedWindow = existingWindow
openedWindow.openLocations(locationsToOpen)
if (openedWindow.isMinimized()) {
openedWindow.restore()
} else {
openedWindow.focus()
}
openedWindow.replaceEnvironment(env)
} else {
let resourcePath, windowInitializationScript
if (devMode) {
try {
windowInitializationScript = require.resolve(
path.join(this.devResourcePath, 'src', 'initialize-application-window')
)
resourcePath = this.devResourcePath
} catch (error) {}
}
if (!windowInitializationScript) {
windowInitializationScript = require.resolve('../initialize-application-window')
}
if (!resourcePath) resourcePath = this.resourcePath
if (!windowDimensions) windowDimensions = this.getDimensionsForNewWindow()
openedWindow = new AtomWindow(this, this.fileRecoveryService, {
initialPaths,
locationsToOpen,
windowInitializationScript,
resourcePath,
devMode,
safeMode,
windowDimensions,
profileStartup,
clearWindowState,
env
})
this.addWindow(openedWindow)
openedWindow.focus()
}
if (pidToKillWhenClosed != null) {
if (!this.waitSessionsByWindow.has(openedWindow)) {
this.waitSessionsByWindow.set(openedWindow, [])
}
this.waitSessionsByWindow.get(openedWindow).push({
pid: pidToKillWhenClosed,
remainingPaths: new Set(pathsToOpen)
})
}
openedWindow.browserWindow.once('closed', () => this.killProcessesForWindow(openedWindow))
return openedWindow
}
// Kill all processes associated with opened windows.
killAllProcesses () {
for (let window of this.waitSessionsByWindow.keys()) {
this.killProcessesForWindow(window)
}
}
killProcessesForWindow (window) {
const sessions = this.waitSessionsByWindow.get(window)
if (!sessions) return
for (const session of sessions) {
this.killProcess(session.pid)
}
this.waitSessionsByWindow.delete(window)
}
windowDidClosePathWithWaitSession (window, initialPath) {
const waitSessions = this.waitSessionsByWindow.get(window)
if (!waitSessions) return
for (let i = waitSessions.length - 1; i >= 0; i--) {
const session = waitSessions[i]
session.remainingPaths.delete(initialPath)
if (session.remainingPaths.size === 0) {
this.killProcess(session.pid)
waitSessions.splice(i, 1)
}
}
}
// Kill the process with the given pid.
killProcess (pid) {
try {
const parsedPid = parseInt(pid)
if (isFinite(parsedPid)) this._killProcess(parsedPid)
} catch (error) {
if (error.code !== 'ESRCH') {
console.log(`Killing process ${pid} failed: ${error.code != null ? error.code : error.message}`)
}
}
}
async saveCurrentWindowOptions (allowEmpty = false) {
if (this.quitting) return
const states = []
for (let window of this.getAllWindows()) {
if (!window.isSpec) states.push({initialPaths: window.representedDirectoryPaths})
}
states.reverse()
if (states.length > 0 || allowEmpty) {
await this.storageFolder.store('application.json', states)
this.emit('application:did-save-state')
}
}
async loadPreviousWindowOptions () {
const states = await this.storageFolder.load('application.json')
if (states) {
return states.map(state => ({
initialPaths: state.initialPaths,
pathsToOpen: state.initialPaths.filter(p => fs.isDirectorySync(p)),
urlsToOpen: [],
devMode: this.devMode,
safeMode: this.safeMode
}))
} else {
return []
}
}
// Open an atom:// url.
//
// The host of the URL being opened is assumed to be the package name
// responsible for opening the URL. A new window will be created with
// that package's `urlMain` as the bootstrap script.
//
// options -
// :urlToOpen - The atom:// url to open.
// :devMode - Boolean to control the opened window's dev mode.
// :safeMode - Boolean to control the opened window's safe mode.
openUrl ({urlToOpen, devMode, safeMode, env}) {
const parsedUrl = url.parse(urlToOpen, true)
if (parsedUrl.protocol !== 'atom:') return
const pack = this.findPackageWithName(parsedUrl.host, devMode)
if (pack && pack.urlMain) {
return this.openPackageUrlMain(
parsedUrl.host,
pack.urlMain,
urlToOpen,
devMode,
safeMode,
env
)
} else {
return this.openPackageUriHandler(urlToOpen, parsedUrl, devMode, safeMode, env)
}
}
openPackageUriHandler (url, parsedUrl, devMode, safeMode, env) {
let bestWindow
if (parsedUrl.host === 'core') {
const predicate = require('../core-uri-handlers').windowPredicate(parsedUrl)
bestWindow = this.getLastFocusedWindow(win => !win.isSpecWindow() && predicate(win))
}
if (!bestWindow) bestWindow = this.getLastFocusedWindow(win => !win.isSpecWindow())
if (bestWindow) {
bestWindow.sendURIMessage(url)
bestWindow.focus()
} else {
let windowInitializationScript
let {resourcePath} = this
if (devMode) {
try {
windowInitializationScript = require.resolve(
path.join(this.devResourcePath, 'src', 'initialize-application-window')
)
resourcePath = this.devResourcePath
} catch (error) {}
}
if (!windowInitializationScript) {
windowInitializationScript = require.resolve('../initialize-application-window')
}
const windowDimensions = this.getDimensionsForNewWindow()
const window = new AtomWindow(this, this.fileRecoveryService, {
resourcePath,
windowInitializationScript,
devMode,
safeMode,
windowDimensions,
env
})
this.addWindow(window)
window.on('window:loaded', () => window.sendURIMessage(url))
return window
}
}
findPackageWithName (packageName, devMode) {
return this.getPackageManager(devMode).getAvailablePackageMetadata().find(({name}) =>
name === packageName
)
}
openPackageUrlMain (packageName, packageUrlMain, urlToOpen, devMode, safeMode, env) {
const packagePath = this.getPackageManager(devMode).resolvePackagePath(packageName)
const windowInitializationScript = path.resolve(packagePath, packageUrlMain)
const windowDimensions = this.getDimensionsForNewWindow()
const window = new AtomWindow(this, this.fileRecoveryService, {
windowInitializationScript,
resourcePath: this.resourcePath,
devMode,
safeMode,
urlToOpen,
windowDimensions,
env
})
this.addWindow(window)
return window
}
getPackageManager (devMode) {
if (this.packages == null) {
const PackageManager = require('../package-manager')
this.packages = new PackageManager({})
this.packages.initialize({
configDirPath: process.env.ATOM_HOME,
devMode,
resourcePath: this.resourcePath
})
}
return this.packages
}
// Opens up a new {AtomWindow} to run specs within.
//
// options -
// :headless - A Boolean that, if true, will close the window upon
// completion.
// :resourcePath - The path to include specs from.
// :specPath - The directory to load specs from.
// :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages
// and ~/.atom/dev/packages, defaults to false.
runTests ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout, env}) {
let windowInitializationScript
if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) {
;({resourcePath} = this)
}
const timeoutInSeconds = Number.parseFloat(timeout)
if (!Number.isNaN(timeoutInSeconds)) {
const timeoutHandler = function () {
console.log(
`The test suite has timed out because it has been running for more than ${timeoutInSeconds} seconds.`
)
return process.exit(124) // Use the same exit code as the UNIX timeout util.
}
setTimeout(timeoutHandler, timeoutInSeconds * 1000)
}
try {
windowInitializationScript = require.resolve(
path.resolve(this.devResourcePath, 'src', 'initialize-test-window')
)
} catch (error) {
windowInitializationScript = require.resolve(
path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window')
)
}
const testPaths = []
if (pathsToOpen != null) {
for (let pathToOpen of pathsToOpen) {
testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)))
}
}
if (testPaths.length === 0) {
process.stderr.write('Error: Specify at least one test path\n\n')
process.exit(1)
}
const legacyTestRunnerPath = this.resolveLegacyTestRunnerPath()
const testRunnerPath = this.resolveTestRunnerPath(testPaths[0])
const devMode = true
const isSpec = true
if (safeMode == null) {
safeMode = false
}
const window = new AtomWindow(this, this.fileRecoveryService, {
windowInitializationScript,
resourcePath,
headless,
isSpec,
devMode,
testRunnerPath,
legacyTestRunnerPath,
testPaths,
logFile,
safeMode,
env
})
this.addWindow(window)
if (env) window.replaceEnvironment(env)
return window
}
runBenchmarks ({headless, test, resourcePath, executedFrom, pathsToOpen, env}) {
let windowInitializationScript
if (resourcePath !== this.resourcePath && !fs.existsSync(resourcePath)) {
;({resourcePath} = this)
}
try {
windowInitializationScript = require.resolve(
path.resolve(this.devResourcePath, 'src', 'initialize-benchmark-window')
)
} catch (error) {
windowInitializationScript = require.resolve(
path.resolve(__dirname, '..', '..', 'src', 'initialize-benchmark-window')
)
}
const benchmarkPaths = []
if (pathsToOpen != null) {
for (let pathToOpen of pathsToOpen) {
benchmarkPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)))
}
}
if (benchmarkPaths.length === 0) {
process.stderr.write('Error: Specify at least one benchmark path.\n\n')
process.exit(1)
}
const devMode = true
const isSpec = true
const safeMode = false
const window = new AtomWindow(this, this.fileRecoveryService, {
windowInitializationScript,
resourcePath,
headless,
test,
isSpec,
devMode,
benchmarkPaths,
safeMode,
env
})
this.addWindow(window)
return window
}
resolveTestRunnerPath (testPath) {
let packageRoot
if (FindParentDir == null) {
FindParentDir = require('find-parent-dir')
}
if ((packageRoot = FindParentDir.sync(testPath, 'package.json'))) {
const packageMetadata = require(path.join(packageRoot, 'package.json'))
if (packageMetadata.atomTestRunner) {
let testRunnerPath
if (Resolve == null) {
Resolve = require('resolve')
}
if (
(testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, {
basedir: packageRoot,
extensions: Object.keys(require.extensions)
}))
) {
return testRunnerPath
} else {
process.stderr.write(
`Error: Could not resolve test runner path '${packageMetadata.atomTestRunner}'`
)
process.exit(1)
}
}
}
return this.resolveLegacyTestRunnerPath()
}
resolveLegacyTestRunnerPath () {
try {
return require.resolve(path.resolve(this.devResourcePath, 'spec', 'jasmine-test-runner'))
} catch (error) {
return require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner'))
}
}
parsePathToOpen (pathToOpen, executedFrom = '') {
let initialColumn, initialLine
if (!pathToOpen) {
return {pathToOpen}
}
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
const match = pathToOpen.match(LocationSuffixRegExp)
if (match != null) {
pathToOpen = pathToOpen.slice(0, -match[0].length)
if (match[1]) {
initialLine = Math.max(0, parseInt(match[1].slice(1)) - 1)
}
if (match[2]) {
initialColumn = Math.max(0, parseInt(match[2].slice(1)) - 1)
}
} else {
initialLine = initialColumn = null
}
const normalizedPath = path.normalize(path.resolve(executedFrom, fs.normalize(pathToOpen)))
const stat = fs.statSyncNoException(normalizedPath)
if (stat || !url.parse(pathToOpen).protocol) pathToOpen = normalizedPath
return {pathToOpen, stat, initialLine, initialColumn}
}
// Opens a native dialog to prompt the user for a path.
//
// Once paths are selected, they're opened in a new or existing {AtomWindow}s.
//
// options -
// :type - A String which specifies the type of the dialog, could be 'file',
// 'folder' or 'all'. The 'all' is only available on macOS.
// :devMode - A Boolean which controls whether any newly opened windows
// should be in dev mode or not.
// :safeMode - A Boolean which controls whether any newly opened windows
// should be in safe mode or not.
// :window - An {AtomWindow} to use for opening a selected file path.
// :path - An optional String which controls the default path to which the
// file dialog opens.
promptForPathToOpen (type, {devMode, safeMode, window}, path = null) {
return this.promptForPath(
type,
pathsToOpen => {
return this.openPaths({pathsToOpen, devMode, safeMode, window})
},
path
)
}
promptForPath (type, callback, path) {
const properties = (() => {
switch (type) {
case 'file': return ['openFile']
case 'folder': return ['openDirectory']
case 'all': return ['openFile', 'openDirectory']
default: throw new Error(`${type} is an invalid type for promptForPath`)
}
})()
// Show the open dialog as child window on Windows and Linux, and as
// independent dialog on macOS. This matches most native apps.
const parentWindow = process.platform === 'darwin' ? null : BrowserWindow.getFocusedWindow()
const openOptions = {
properties: properties.concat(['multiSelections', 'createDirectory']),
title: (() => {
switch (type) {
case 'file': return 'Open File'
case 'folder': return 'Open Folder'
default: return 'Open'
}
})()
}
// File dialog defaults to project directory of currently active editor
if (path) openOptions.defaultPath = path
dialog.showOpenDialog(parentWindow, openOptions, callback)
}
promptForRestart () {
dialog.showMessageBox(BrowserWindow.getFocusedWindow(), {
type: 'warning',
title: 'Restart required',
message: 'You will need to restart Atom for this change to take effect.',
buttons: ['Restart Atom', 'Cancel']
}, response => { if (response === 0) this.restart() })
}
restart () {
const args = []
if (this.safeMode) args.push('--safe')
if (this.logFile != null) args.push(`--log-file=${this.logFile}`)
if (this.socketPath != null) args.push(`--socket-path=${this.socketPath}`)
if (this.userDataDir != null) args.push(`--user-data-dir=${this.userDataDir}`)
if (this.devMode) {
args.push('--dev')
args.push(`--resource-path=${this.resourcePath}`)
}
app.relaunch({args})
app.quit()
}
disableZoomOnDisplayChange () {
const callback = () => {
this.getAllWindows().map(window => window.disableZoom())
}
// Set the limits every time a display is added or removed, otherwise the
// configuration gets reset to the default, which allows zooming the
// webframe.
screen.on('display-added', callback)
screen.on('display-removed', callback)
return new Disposable(() => {
screen.removeListener('display-added', callback)
screen.removeListener('display-removed', callback)
})
}
}
class WindowStack {
constructor (windows = []) {
this.addWindow = this.addWindow.bind(this)
this.touch = this.touch.bind(this)
this.removeWindow = this.removeWindow.bind(this)
this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this)
this.all = this.all.bind(this)
this.windows = windows
}
addWindow (window) {
this.removeWindow(window)
return this.windows.unshift(window)
}
touch (window) {
return this.addWindow(window)
}
removeWindow (window) {
const currentIndex = this.windows.indexOf(window)
if (currentIndex > -1) {
return this.windows.splice(currentIndex, 1)
}
}
getLastFocusedWindow (predicate) {
if (predicate == null) {
predicate = win => true
}
return this.windows.find(predicate)
}
all () {
return this.windows
}
}