Files
atom/src/main-process/atom-application.js
2019-04-24 15:22:32 -04:00

1672 lines
54 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+)?$/
// Increment this when changing the serialization format of `${ATOM_HOME}/storage/application.json` used by
// AtomApplication::saveCurrentWindowOptions() and AtomApplication::loadPreviousWindowOptions() in a backward-
// incompatible way.
const APPLICATION_STATE_VERSION = '1'
const getDefaultPath = () => {
const editor = atom.workspace.getActiveTextEditor()
if (!editor || !editor.getPath()) {
return
}
const paths = atom.project.getPaths()
if (paths) {
return paths[0]
}
}
const getSocketSecretPath = (atomVersion) => {
const {username} = os.userInfo()
const atomHome = path.resolve(process.env.ATOM_HOME)
return path.join(atomHome, `.atom-socket-secret-${username}-${atomVersion}`)
}
const getSocketPath = (socketSecret) => {
if (!socketSecret) {
return null
}
// Hash the secret to create the socket name to not expose it.
const socketName = crypto
.createHmac('sha256', socketSecret)
.update('socketName')
.digest('hex')
.substr(0, 12)
if (process.platform === 'win32') {
return `\\\\.\\pipe\\atom-${socketName}-sock`
} else {
return path.join(os.tmpdir(), `atom-${socketName}.sock`)
}
}
const getExistingSocketSecret = (atomVersion) => {
const socketSecretPath = getSocketSecretPath(atomVersion)
if (!fs.existsSync(socketSecretPath)) {
return null
}
return fs.readFileSync(socketSecretPath, 'utf8')
}
const createSocketSecret = (atomVersion) => {
const socketSecret = crypto.randomBytes(16).toString('hex')
fs.writeFileSync(getSocketSecretPath(atomVersion), socketSecret, {encoding: 'utf8', mode: 0o600})
return socketSecret
}
const encryptOptions = (options, secret) => {
const message = JSON.stringify(options)
const initVector = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-gcm', secret, initVector)
let content = cipher.update(message, 'utf8', 'hex')
content += cipher.final('hex')
const authTag = cipher.getAuthTag().toString('hex')
return JSON.stringify({
authTag,
content,
initVector: initVector.toString('hex')
})
}
const decryptOptions = (optionsMessage, secret) => {
const {authTag, content, initVector} = JSON.parse(optionsMessage)
const decipher = crypto.createDecipheriv('aes-256-gcm', secret, Buffer.from(initVector, 'hex'))
decipher.setAuthTag(Buffer.from(authTag, 'hex'))
let message = decipher.update(content, 'hex', 'utf8')
message += decipher.final('utf8')
return JSON.parse(message)
}
// 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) {
const socketSecret = getExistingSocketSecret(options.version)
const socketPath = getSocketPath(socketSecret)
const createApplication = options.createApplication || (async () => {
const app = new AtomApplication(options)
await app.initialize(options)
return app
})
// 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 (
!socketPath || options.test || options.benchmark || options.benchmarkTest ||
(process.platform !== 'win32' && !fs.existsSync(socketPath))
) {
return createApplication(options)
}
return new Promise(resolve => {
const client = net.connect({path: socketPath}, () => {
client.write(encryptOptions(options, socketSecret), () => {
client.end()
app.quit()
resolve(null)
})
})
client.on('error', () => resolve(createApplication(options)))
})
}
exit (status) {
app.exit(status)
}
constructor (options) {
super()
this.quitting = false
this.quittingForUpdate = 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.logFile = options.logFile
this.userDataDir = options.userDataDir
this._killProcess = options.killProcess || process.kill.bind(process)
if (!options.test && !options.benchmark && !options.benchmarkTest) {
this.socketSecret = createSocketSecret(this.version)
this.socketPath = getSocketPath(this.socketSecret)
}
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())
}
let optionsForWindowsToOpen = []
let shouldReopenPreviousWindows = false
if (options.test || options.benchmark || options.benchmarkTest) {
optionsForWindowsToOpen.push(options)
} else if (options.newWindow) {
shouldReopenPreviousWindows = false
} 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) {
optionsForWindowsToOpen = [...await this.loadPreviousWindowOptions(), ...optionsForWindowsToOpen]
}
if (optionsForWindowsToOpen.length === 0) {
optionsForWindowsToOpen.push(options)
}
// Preserve window opening order
const windows = []
for (const options of optionsForWindowsToOpen) {
windows.push(await this.openWithOptions(options))
}
return windows
}
openWithOptions (options) {
const {
pathsToOpen,
executedFrom,
foldersToOpen,
urlsToOpen,
benchmark,
benchmarkTest,
test,
pidToKillWhenClosed,
devMode,
safeMode,
newWindow,
logFile,
profileStartup,
timeout,
clearWindowState,
addToLastWindow,
preserveFocus,
env
} = options
if (!preserveFocus) {
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 && pathsToOpen.length > 0) || (foldersToOpen && foldersToOpen.length > 0)) {
return this.openPaths({
pathsToOpen,
foldersToOpen,
executedFrom,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
clearWindowState,
addToLastWindow,
env
})
} else if (urlsToOpen && urlsToOpen.length > 0) {
return Promise.all(
urlsToOpen.map(urlToOpen => this.openUrl({urlToOpen, devMode, safeMode, env}))
)
} else {
// Always open an editor window if this is the first instance of Atom.
return this.openPath({
pathToOpen: null,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
clearWindowState,
addToLastWindow,
env
})
}
}
// Public: Create a new {AtomWindow} bound to this application.
createWindow (settings) {
return new AtomWindow(this, this.fileRecoveryService, settings)
}
// 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)
this.saveCurrentWindowOptions(false)
}
}
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', () => {
try {
const options = decryptOptions(data, this.socketSecret)
this.openWithOptions(options)
} catch (e) {
// Error while parsing/decrypting the options passed by the client.
// We cannot trust the client, aborting.
}
})
})
return new Promise(resolve => {
server.listen(this.socketPath, resolve)
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
}
}
}
deleteSocketSecretFile () {
if (!this.socketSecret) {
return
}
const socketSecretPath = getSocketSecretPath(this.version)
if (fs.existsSync(socketSecretPath)) {
try {
fs.unlinkSync(socketSecretPath)
} catch (error) {
// Ignore ENOENT errors in case the file was deleted between the exists
// check and the call to unlink sync.
if (error.code !== 'ENOENT') throw error
}
}
}
// Registers basic application commands, non-idempotent.
handleEvents () {
const createOpenSettings = ({event, sameWindow}) => {
const targetWindow = event ? this.atomWindowForEvent(event) : this.focusedWindow()
return {
devMode: targetWindow ? targetWindow.devMode : false,
safeMode: targetWindow ? targetWindow.safeMode : false,
window: sameWindow && targetWindow ? targetWindow : null
}
}
this.on('application:quit', () => app.quit())
this.on('application:new-window', () => this.openPath(createOpenSettings({})))
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.quittingForUpdate = true
this.autoUpdateManager.install()
})
this.on('application:check-for-update', () => this.autoUpdateManager.check())
if (process.platform === 'darwin') {
this.on('application:reopen-project', ({ paths }) => {
this.openPaths({ pathsToOpen: paths })
})
this.on('application:open', () => {
this.promptForPathToOpen('all', createOpenSettings({sameWindow: true}), getDefaultPath())
})
this.on('application:open-file', () => {
this.promptForPathToOpen('file', createOpenSettings({sameWindow: true}), getDefaultPath())
})
this.on('application:open-folder', () => {
this.promptForPathToOpen('folder', createOpenSettings({sameWindow: true}), getDefaultPath())
})
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.deleteSocketSecretFile()
}))
// Triggered by the 'open-file' event from Electron:
// https://electronjs.org/docs/api/app#event-open-file-macos
// For example, this is fired when a file is dragged and dropped onto the Atom application icon in the dock.
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 set of paths using the standard window location logic.
// Used for application:reopen-project.
this.disposable.add(ipcHelpers.on(ipcMain, 'open', (_event, options) => {
if (options) {
if (typeof options.pathsToOpen === 'string') {
options.pathsToOpen = [options.pathsToOpen]
}
if (options.pathsToOpen && options.pathsToOpen.length > 0) {
this.openPaths(options)
} else {
this.addWindow(this.createWindow(options))
}
} else {
this.promptForPathToOpen('all', {window})
}
}))
// Prompt for a file, folder, or either, then open the chosen paths. Files will be opened in the originating
// window; folders will be opened in a new window unless an existing window exactly contains all of them.
this.disposable.add(ipcHelpers.on(ipcMain, 'open-chosen-any', (event, defaultPath) => {
this.promptForPathToOpen('all', createOpenSettings({event, sameWindow: true}), defaultPath)
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'open-chosen-file', (event, defaultPath) => {
this.promptForPathToOpen('file', createOpenSettings({event, sameWindow: true}), defaultPath)
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'open-chosen-folder', (event, defaultPath) => {
this.promptForPathToOpen('folder', createOpenSettings({event}), defaultPath)
}))
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, 'window-command', (event, command, ...args) => {
const window = BrowserWindow.fromWebContents(event.sender)
return window && 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(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 locations.
windowForLocations (locationsToOpen, devMode, safeMode) {
return this.getLastFocusedWindow(window =>
window.devMode === devMode &&
window.safeMode === safeMode &&
window.containsLocations(locationsToOpen)
)
}
// 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 ({
pathToOpen,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
profileStartup,
window,
clearWindowState,
addToLastWindow,
env
} = {}) {
return this.openPaths({
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
// :foldersToOpen - An array of additional paths to open that must be existing directories
// :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.
async openPaths ({
pathsToOpen,
foldersToOpen,
executedFrom,
pidToKillWhenClosed,
newWindow,
devMode,
safeMode,
windowDimensions,
profileStartup,
window,
clearWindowState,
addToLastWindow,
env
} = {}) {
if (!env) env = process.env
if (!pathsToOpen) pathsToOpen = []
if (!foldersToOpen) foldersToOpen = []
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
clearWindowState = Boolean(clearWindowState)
const locationsToOpen = await Promise.all(
pathsToOpen.map(pathToOpen => this.parsePathToOpen(pathToOpen, executedFrom, {
hasWaitSession: pidToKillWhenClosed != null
}))
)
for (const folderToOpen of foldersToOpen) {
locationsToOpen.push({
pathToOpen: folderToOpen,
initialLine: null,
initialColumn: null,
exists: true,
isDirectory: true,
hasWaitSession: pidToKillWhenClosed != null
})
}
if (locationsToOpen.length === 0) {
return
}
const hasNonEmptyPath = locationsToOpen.some(location => location.pathToOpen)
const createNewWindow = newWindow || !hasNonEmptyPath
let existingWindow
if (!createNewWindow) {
// An explicitly provided AtomWindow has precedence.
existingWindow = window
// If no window is specified and at least one path is provided, locate an existing window that contains all
// provided paths.
if (!existingWindow && hasNonEmptyPath) {
existingWindow = this.windowForLocations(locationsToOpen, devMode, safeMode)
}
// No window specified, no existing window found, and addition to the last window requested. Find the last
// focused window that matches the requested dev and safe modes.
if (!existingWindow && addToLastWindow) {
existingWindow = this.getLastFocusedWindow(win => {
return win.devMode === devMode && win.safeMode === safeMode
})
}
// Fall back to the last focused window that has no project roots.
if (!existingWindow) {
existingWindow = this.getLastFocusedWindow(win => !win.hasProjectPaths())
}
// One last case: if *no* paths are directories, add to the last focused window.
if (!existingWindow) {
const noDirectories = locationsToOpen.every(location => !location.isDirectory)
if (noDirectories) {
existingWindow = this.getLastFocusedWindow(win => {
return win.devMode === devMode && win.safeMode === safeMode
})
}
}
}
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 = this.createWindow({
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(locationsToOpen.map(location => location.pathToOpen).filter(Boolean))
})
}
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 state = {
version: APPLICATION_STATE_VERSION,
windows: this.getAllWindows()
.filter(window => !window.isSpec)
.map(window => ({projectRoots: window.projectRoots}))
}
state.windows.reverse()
if (state.windows.length > 0 || allowEmpty) {
await this.storageFolder.store('application.json', state)
this.emit('application:did-save-state')
}
}
async loadPreviousWindowOptions () {
const state = await this.storageFolder.load('application.json')
if (!state) {
return []
}
if (state.version === APPLICATION_STATE_VERSION) {
// Atom >=1.36.1
// Schema: {version: '1', windows: [{projectRoots: ['<root-dir>', ...]}, ...]}
return state.windows.map(each => ({
foldersToOpen: each.projectRoots,
devMode: this.devMode,
safeMode: this.safeMode,
newWindow: true
}))
} else if (state.version === undefined) {
// Atom <= 1.36.0
// Schema: [{initialPaths: ['<root-dir>', ...]}, ...]
return await Promise.all(
state.map(async windowState => {
// Classify each window's initialPaths as directories or non-directories
const classifiedPaths = await Promise.all(
windowState.initialPaths.map(initialPath => new Promise(resolve => {
fs.isDirectory(initialPath, isDir => resolve({initialPath, isDir}))
}))
)
// Only accept initialPaths that are existing directories
return {
foldersToOpen: classifiedPaths.filter(({isDir}) => isDir).map(({initialPath}) => initialPath),
devMode: this.devMode,
safeMode: this.safeMode,
newWindow: true
}
})
)
} else {
// Unrecognized version (from a newer Atom?)
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()
return bestWindow
} 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 = this.createWindow({
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 = this.createWindow({
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 = this.createWindow({
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 = this.createWindow({
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'))
}
}
async parsePathToOpen (pathToOpen, executedFrom, extra) {
const result = Object.assign({
pathToOpen,
initialColumn: null,
initialLine: null,
exists: false,
isDirectory: false,
isFile: false
}, extra)
if (!pathToOpen) {
return result
}
result.pathToOpen = result.pathToOpen.replace(/[:\s]+$/, '')
const match = result.pathToOpen.match(LocationSuffixRegExp)
if (match != null) {
result.pathToOpen = result.pathToOpen.slice(0, -match[0].length)
if (match[1]) {
result.initialLine = Math.max(0, parseInt(match[1].slice(1), 10) - 1)
}
if (match[2]) {
result.initialColumn = Math.max(0, parseInt(match[2].slice(1), 10) - 1)
}
}
const normalizedPath = path.normalize(path.resolve(executedFrom, fs.normalize(result.pathToOpen)))
if (!url.parse(pathToOpen).protocol) {
result.pathToOpen = normalizedPath
}
await new Promise((resolve, reject) => {
fs.stat(result.pathToOpen, (err, st) => {
if (err) {
if (err.code === 'ENOENT' || err.code === 'EACCES') {
result.exists = false
resolve()
} else {
reject(err)
}
return
}
result.exists = true
result.isFile = st.isFile()
result.isDirectory = st.isDirectory()
resolve()
})
})
return result
}
// 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 selected file paths as long as
// all are files.
// :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,
async pathsToOpen => {
let targetWindow
// Open in :window as long as no chosen paths are folders. If any chosen path is a folder, open in a
// new window instead.
if (type === 'folder') {
targetWindow = null
} else if (type === 'file') {
targetWindow = window
} else if (type === 'all') {
const areDirectories = await Promise.all(
pathsToOpen.map(pathToOpen => new Promise(resolve => fs.isDirectory(pathToOpen, resolve)))
)
if (!areDirectories.some(Boolean)) {
targetWindow = window
}
}
return this.openPaths({pathsToOpen, devMode, safeMode, window: targetWindow})
},
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 an 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.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
}
}