mirror of https://github.com/atom/atom.git
2117 lines
61 KiB
JavaScript
2117 lines
61 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 StartupTime = require('../startup-time');
|
|
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 { promisify } = require('util');
|
|
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 getRandomBytes = promisify(crypto.randomBytes);
|
|
const writeFile = promisify(fs.writeFile);
|
|
|
|
const createSocketSecret = async atomVersion => {
|
|
const socketSecret = (await getRandomBytes(16)).toString('hex');
|
|
|
|
await writeFile(getSocketSecretPath(atomVersion), socketSecret, {
|
|
encoding: 'utf8',
|
|
mode: 0o600
|
|
});
|
|
|
|
return socketSecret;
|
|
};
|
|
|
|
const encryptOptions = (options, secret) => {
|
|
const message = JSON.stringify(options);
|
|
|
|
// Even if the following IV is not cryptographically secure, there's a really good chance
|
|
// it's going to be unique between executions which is the requirement for GCM.
|
|
// We're not using `crypto.randomBytes()` because in electron v2, that API is really slow
|
|
// on Windows machines, which affects the startup time of Atom.
|
|
// TodoElectronIssue: Once we upgrade to electron v3 we can use `crypto.randomBytes()`
|
|
const initVectorHash = crypto.createHash('sha1');
|
|
initVectorHash.update(Date.now() + '');
|
|
initVectorHash.update(Math.random() + '');
|
|
const initVector = initVectorHash.digest();
|
|
|
|
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) {
|
|
StartupTime.addMarker('main-process:atom-application:open');
|
|
|
|
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) {
|
|
StartupTime.addMarker('main-process:atom-application:constructor:start');
|
|
|
|
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);
|
|
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();
|
|
|
|
StartupTime.addMarker('main-process:atom-application:constructor:end');
|
|
}
|
|
|
|
// 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) {
|
|
StartupTime.addMarker('main-process:atom-application:initialize:start');
|
|
|
|
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
|
|
);
|
|
|
|
// Don't await for the following method to avoid delaying the opening of a new window.
|
|
// (we await it just after opening it).
|
|
// We need to do this because `listenForArgumentsFromNewProcess()` calls `crypto.randomBytes`,
|
|
// which is really slow on Windows machines.
|
|
// (TodoElectronIssue: This got fixed in electron v3: https://github.com/electron/electron/issues/2073).
|
|
let socketServerPromise;
|
|
if (options.test || options.benchmark || options.benchmarkTest) {
|
|
socketServerPromise = Promise.resolve();
|
|
} else {
|
|
socketServerPromise = this.listenForArgumentsFromNewProcess();
|
|
}
|
|
|
|
this.setupDockMenu();
|
|
|
|
const result = await this.launch(options);
|
|
this.autoUpdateManager.initialize();
|
|
await socketServerPromise;
|
|
|
|
StartupTime.addMarker('main-process:atom-application:initialize:end');
|
|
|
|
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().then(disposable => {
|
|
this.disposable.add(disposable);
|
|
this.config.onDidChange('core.titleBar', () => this.promptForRestart());
|
|
this.config.onDidChange('core.colorProfile', () =>
|
|
this.promptForRestart()
|
|
);
|
|
});
|
|
|
|
// TodoElectronIssue: In electron v2 awaiting the watcher causes some delay
|
|
// in Windows machines, which affects directly the startup time.
|
|
if (process.platform !== 'win32') {
|
|
await this.configFilePromise;
|
|
}
|
|
}
|
|
|
|
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.
|
|
async listenForArgumentsFromNewProcess() {
|
|
this.socketSecretPromise = createSocketSecret(this.version);
|
|
this.socketSecret = await this.socketSecretPromise;
|
|
this.socketPath = getSocketPath(this.socketSecret);
|
|
|
|
await 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)
|
|
);
|
|
});
|
|
}
|
|
|
|
async deleteSocketFile() {
|
|
if (process.platform === 'win32') return;
|
|
|
|
if (!this.socketSecretPromise) {
|
|
return;
|
|
}
|
|
await this.socketSecretPromise;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
async deleteSocketSecretFile() {
|
|
if (!this.socketSecretPromise) {
|
|
return;
|
|
}
|
|
await this.socketSecretPromise;
|
|
|
|
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);
|
|
} else {
|
|
console.error(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();
|
|
|
|
return Promise.all([
|
|
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.here) {
|
|
options.window = this.atomWindowForEvent(event);
|
|
}
|
|
|
|
if (options.pathsToOpen && options.pathsToOpen.length > 0) {
|
|
this.openPaths(options);
|
|
} else {
|
|
this.addWindow(this.createWindow(options));
|
|
}
|
|
} else {
|
|
this.promptForPathToOpen('all', {});
|
|
}
|
|
})
|
|
);
|
|
|
|
// 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.isSpec &&
|
|
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.isSpec && 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.isSpec && !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.isSpec &&
|
|
win.devMode === devMode &&
|
|
win.safeMode === safeMode
|
|
);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
let openedWindow;
|
|
if (existingWindow) {
|
|
openedWindow = existingWindow;
|
|
StartupTime.addMarker('main-process:atom-application:open-in-existing');
|
|
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();
|
|
|
|
StartupTime.addMarker('main-process:atom-application:create-window');
|
|
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 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;
|
|
}
|
|
}
|