@theia/application-manager 1.48.1 → 1.48.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/rebuild.ts CHANGED
@@ -1,345 +1,345 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2017 TypeFox and others.
3
- //
4
- // This program and the accompanying materials are made available under the
5
- // terms of the Eclipse Public License v. 2.0 which is available at
6
- // http://www.eclipse.org/legal/epl-2.0.
7
- //
8
- // This Source Code may also be made available under the following Secondary
9
- // Licenses when the conditions for such availability set forth in the Eclipse
10
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- // with the GNU Classpath Exception which is available at
12
- // https://www.gnu.org/software/classpath/license.html.
13
- //
14
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
- // *****************************************************************************
16
-
17
- import cp = require('child_process');
18
- import fs = require('fs-extra');
19
- import path = require('path');
20
- import os = require('os');
21
-
22
- export type RebuildTarget = 'electron' | 'browser' | 'browser-only';
23
-
24
- const EXIT_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'];
25
-
26
- interface ExitToken {
27
- getLastSignal(): NodeJS.Signals | undefined
28
- onSignal(callback: (signal: NodeJS.Signals) => void): void
29
- }
30
-
31
- type NodeABI = string | number;
32
-
33
- export const DEFAULT_MODULES = [
34
- 'node-pty',
35
- 'nsfw',
36
- 'native-keymap',
37
- 'find-git-repositories',
38
- 'drivelist',
39
- 'keytar',
40
- 'ssh2',
41
- 'cpu-features'
42
- ];
43
-
44
- export interface RebuildOptions {
45
- /**
46
- * What modules to rebuild.
47
- */
48
- modules?: string[]
49
- /**
50
- * Folder where the module cache will be created/read from.
51
- */
52
- cacheRoot?: string
53
- /**
54
- * In the event that `node-abi` doesn't recognize the current Electron version,
55
- * you can specify the Node ABI to rebuild for.
56
- */
57
- forceAbi?: NodeABI
58
- }
59
-
60
- /**
61
- * @param target What to rebuild for.
62
- * @param options
63
- */
64
- export function rebuild(target: RebuildTarget, options: RebuildOptions = {}): void {
65
- const {
66
- modules = DEFAULT_MODULES,
67
- cacheRoot = process.cwd(),
68
- forceAbi,
69
- } = options;
70
- const cache = path.resolve(cacheRoot, '.browser_modules');
71
- const cacheExists = folderExists(cache);
72
- guardExit(async token => {
73
- if (target === 'electron' && !cacheExists) {
74
- process.exitCode = await rebuildElectronModules(cache, modules, forceAbi, token);
75
- } else if (target === 'browser' && cacheExists) {
76
- process.exitCode = await revertBrowserModules(cache, modules);
77
- } else {
78
- console.log(`native node modules are already rebuilt for ${target}`);
79
- }
80
- }).catch(errorOrSignal => {
81
- if (typeof errorOrSignal === 'string' && errorOrSignal in os.constants.signals) {
82
- process.kill(process.pid, errorOrSignal);
83
- } else {
84
- throw errorOrSignal;
85
- }
86
- });
87
- }
88
-
89
- function folderExists(folder: string): boolean {
90
- if (fs.existsSync(folder)) {
91
- if (fs.statSync(folder).isDirectory()) {
92
- return true;
93
- } else {
94
- throw new Error(`"${folder}" exists but it is not a directory`);
95
- }
96
- }
97
- return false;
98
- }
99
-
100
- /**
101
- * Schema for `<browserModuleCache>/modules.json`.
102
- */
103
- interface ModulesJson {
104
- [moduleName: string]: ModuleBackup
105
- }
106
- interface ModuleBackup {
107
- originalLocation: string
108
- }
109
-
110
- async function rebuildElectronModules(browserModuleCache: string, modules: string[], forceAbi: NodeABI | undefined, token: ExitToken): Promise<number> {
111
- const modulesJsonPath = path.join(browserModuleCache, 'modules.json');
112
- const modulesJson: ModulesJson = await fs.access(modulesJsonPath).then(
113
- () => fs.readJson(modulesJsonPath),
114
- () => ({})
115
- );
116
- let success = true;
117
- // Backup already built browser modules.
118
- await Promise.all(modules.map(async module => {
119
- let modulePath;
120
- try {
121
- modulePath = require.resolve(`${module}/package.json`, {
122
- paths: [process.cwd()],
123
- });
124
- } catch (_) {
125
- console.debug(`Module not found: ${module}`);
126
- return; // Skip current module.
127
- }
128
- const src = path.dirname(modulePath);
129
- const dest = path.join(browserModuleCache, module);
130
- try {
131
- await fs.remove(dest);
132
- await fs.copy(src, dest, { overwrite: true });
133
- modulesJson[module] = {
134
- originalLocation: src,
135
- };
136
- console.debug(`Processed "${module}"`);
137
- } catch (error) {
138
- console.error(`Error while doing a backup for "${module}": ${error}`);
139
- success = false;
140
- }
141
- }));
142
- if (Object.keys(modulesJson).length === 0) {
143
- console.debug('No module to rebuild.');
144
- return 0;
145
- }
146
- // Update manifest tracking the backups' original locations.
147
- await fs.writeJson(modulesJsonPath, modulesJson, { spaces: 2 });
148
- // If we failed to process a module then exit now.
149
- if (!success) {
150
- return 1;
151
- }
152
- const todo = modules.map(m => {
153
- // electron-rebuild ignores the module namespace...
154
- const slash = m.indexOf('/');
155
- return m.startsWith('@') && slash !== -1
156
- ? m.substring(slash + 1)
157
- : m;
158
- });
159
- let exitCode: number | undefined;
160
- try {
161
- if (process.env.THEIA_REBUILD_NO_WORKAROUND) {
162
- exitCode = await runElectronRebuild(todo, forceAbi, token);
163
- } else {
164
- exitCode = await electronRebuildExtraModulesWorkaround(process.cwd(), todo, () => runElectronRebuild(todo, forceAbi, token), token);
165
- }
166
- } catch (error) {
167
- console.error(error);
168
- } finally {
169
- // If code is undefined or different from zero we need to revert back to the browser modules.
170
- if (exitCode !== 0) {
171
- await revertBrowserModules(browserModuleCache, modules);
172
- }
173
- return exitCode ?? 1;
174
- }
175
- }
176
-
177
- async function revertBrowserModules(browserModuleCache: string, modules: string[]): Promise<number> {
178
- let exitCode = 0;
179
- const modulesJsonPath = path.join(browserModuleCache, 'modules.json');
180
- const modulesJson: ModulesJson = await fs.readJson(modulesJsonPath);
181
- await Promise.all(Object.entries(modulesJson).map(async ([moduleName, entry]) => {
182
- if (!modules.includes(moduleName)) {
183
- return; // Skip modules that weren't requested.
184
- }
185
- const src = path.join(browserModuleCache, moduleName);
186
- if (!await fs.pathExists(src)) {
187
- delete modulesJson[moduleName];
188
- console.error(`Missing backup for ${moduleName}!`);
189
- exitCode = 1;
190
- return;
191
- }
192
- const dest = entry.originalLocation;
193
- try {
194
- await fs.remove(dest);
195
- await fs.copy(src, dest, { overwrite: false });
196
- await fs.remove(src);
197
- delete modulesJson[moduleName];
198
- console.debug(`Reverted "${moduleName}"`);
199
- } catch (error) {
200
- console.error(`Error while reverting "${moduleName}": ${error}`);
201
- exitCode = 1;
202
- }
203
- }));
204
- if (Object.keys(modulesJson).length === 0) {
205
- // We restored everything, so we can delete the cache.
206
- await fs.remove(browserModuleCache);
207
- } else {
208
- // Some things were not restored, so we update the manifest.
209
- await fs.writeJson(modulesJsonPath, modulesJson, { spaces: 2 });
210
- }
211
- return exitCode;
212
- }
213
-
214
- async function runElectronRebuild(modules: string[], forceAbi: NodeABI | undefined, token: ExitToken): Promise<number> {
215
- const todo = modules.join(',');
216
- return new Promise(async (resolve, reject) => {
217
- let command = `npx --no-install electron-rebuild -f -w=${todo} -o=${todo}`;
218
- if (forceAbi) {
219
- command += ` --force-abi ${forceAbi}`;
220
- }
221
- const electronRebuild = cp.spawn(command, {
222
- stdio: 'inherit',
223
- shell: true,
224
- });
225
- token.onSignal(signal => electronRebuild.kill(signal));
226
- electronRebuild.on('error', reject);
227
- electronRebuild.on('close', (code, signal) => {
228
- if (signal) {
229
- reject(new Error(`electron-rebuild exited with "${signal}"`));
230
- } else {
231
- resolve(code!);
232
- }
233
- });
234
- });
235
- }
236
-
237
- /**
238
- * `electron-rebuild` is supposed to accept a list of modules to build, even when not part of the dependencies.
239
- * But there is a bug that causes `electron-rebuild` to not correctly process this list of modules.
240
- *
241
- * This workaround will temporarily modify the current package.json file.
242
- *
243
- * PR with fix: https://github.com/electron/electron-rebuild/pull/888
244
- *
245
- * TODO: Remove this workaround.
246
- */
247
- async function electronRebuildExtraModulesWorkaround<T>(cwd: string, extraModules: string[], run: (token: ExitToken) => Promise<T>, token: ExitToken): Promise<T> {
248
- const packageJsonPath = path.resolve(cwd, 'package.json');
249
- if (await fs.pathExists(packageJsonPath)) {
250
- // package.json exists: We back it up before modifying it then revert it.
251
- const packageJsonCopyPath = `${packageJsonPath}.copy`;
252
- const packageJson = await fs.readJson(packageJsonPath);
253
- await fs.copy(packageJsonPath, packageJsonCopyPath);
254
- await throwIfSignal(token, async () => {
255
- await fs.unlink(packageJsonCopyPath);
256
- });
257
- if (typeof packageJson.dependencies !== 'object') {
258
- packageJson.dependencies = {};
259
- }
260
- for (const extraModule of extraModules) {
261
- if (!packageJson.dependencies[extraModule]) {
262
- packageJson.dependencies[extraModule] = '*';
263
- }
264
- }
265
- try {
266
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
267
- await throwIfSignal(token);
268
- return await run(token);
269
- } finally {
270
- await fs.move(packageJsonCopyPath, packageJsonPath, { overwrite: true });
271
- }
272
- } else {
273
- // package.json does not exist: We create one then remove it.
274
- const packageJson = {
275
- name: 'theia-rebuild-workaround',
276
- version: '0.0.0',
277
- dependencies: {} as Record<string, string>,
278
- };
279
- for (const extraModule of extraModules) {
280
- packageJson.dependencies[extraModule] = '*';
281
- }
282
- try {
283
- await fs.writeJson(packageJsonPath, packageJson);
284
- await throwIfSignal(token);
285
- return await run(token);
286
- } finally {
287
- await fs.unlink(packageJsonPath);
288
- }
289
- }
290
- }
291
-
292
- /**
293
- * Temporarily install hooks to **try** to prevent the process from exiting while `run` is running.
294
- *
295
- * Note that it is still possible to kill the process and prevent cleanup logic (e.g. SIGKILL, computer forced shutdown, etc).
296
- */
297
- async function guardExit<T>(run: (token: ExitToken) => Promise<T>): Promise<T> {
298
- const token = new ExitTokenImpl();
299
- const signalListener = (signal: NodeJS.Signals) => token._emitSignal(signal);
300
- for (const signal of EXIT_SIGNALS) {
301
- process.on(signal, signalListener);
302
- }
303
- try {
304
- return await run(token);
305
- } finally {
306
- for (const signal of EXIT_SIGNALS) {
307
- process.off(signal, signalListener);
308
- }
309
- }
310
- }
311
-
312
- class ExitTokenImpl implements ExitToken {
313
-
314
- protected _listeners = new Set<(signal: NodeJS.Signals) => void>();
315
- protected _lastSignal?: NodeJS.Signals;
316
-
317
- onSignal(callback: (signal: NodeJS.Signals) => void): void {
318
- this._listeners.add(callback);
319
- }
320
-
321
- getLastSignal(): NodeJS.Signals | undefined {
322
- return this._lastSignal;
323
- }
324
-
325
- _emitSignal(signal: NodeJS.Signals): void {
326
- this._lastSignal = signal;
327
- for (const listener of this._listeners) {
328
- listener(signal);
329
- }
330
- }
331
- }
332
-
333
- /**
334
- * Throw `signal` if one was received, runs `cleanup` before doing so.
335
- */
336
- async function throwIfSignal(token: ExitToken, cleanup?: () => Promise<void>): Promise<void> {
337
- if (token.getLastSignal()) {
338
- try {
339
- await cleanup?.();
340
- } finally {
341
- // eslint-disable-next-line no-throw-literal
342
- throw token.getLastSignal()!;
343
- }
344
- }
345
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2017 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import cp = require('child_process');
18
+ import fs = require('fs-extra');
19
+ import path = require('path');
20
+ import os = require('os');
21
+
22
+ export type RebuildTarget = 'electron' | 'browser' | 'browser-only';
23
+
24
+ const EXIT_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'];
25
+
26
+ interface ExitToken {
27
+ getLastSignal(): NodeJS.Signals | undefined
28
+ onSignal(callback: (signal: NodeJS.Signals) => void): void
29
+ }
30
+
31
+ type NodeABI = string | number;
32
+
33
+ export const DEFAULT_MODULES = [
34
+ 'node-pty',
35
+ 'nsfw',
36
+ 'native-keymap',
37
+ 'find-git-repositories',
38
+ 'drivelist',
39
+ 'keytar',
40
+ 'ssh2',
41
+ 'cpu-features'
42
+ ];
43
+
44
+ export interface RebuildOptions {
45
+ /**
46
+ * What modules to rebuild.
47
+ */
48
+ modules?: string[]
49
+ /**
50
+ * Folder where the module cache will be created/read from.
51
+ */
52
+ cacheRoot?: string
53
+ /**
54
+ * In the event that `node-abi` doesn't recognize the current Electron version,
55
+ * you can specify the Node ABI to rebuild for.
56
+ */
57
+ forceAbi?: NodeABI
58
+ }
59
+
60
+ /**
61
+ * @param target What to rebuild for.
62
+ * @param options
63
+ */
64
+ export function rebuild(target: RebuildTarget, options: RebuildOptions = {}): void {
65
+ const {
66
+ modules = DEFAULT_MODULES,
67
+ cacheRoot = process.cwd(),
68
+ forceAbi,
69
+ } = options;
70
+ const cache = path.resolve(cacheRoot, '.browser_modules');
71
+ const cacheExists = folderExists(cache);
72
+ guardExit(async token => {
73
+ if (target === 'electron' && !cacheExists) {
74
+ process.exitCode = await rebuildElectronModules(cache, modules, forceAbi, token);
75
+ } else if (target === 'browser' && cacheExists) {
76
+ process.exitCode = await revertBrowserModules(cache, modules);
77
+ } else {
78
+ console.log(`native node modules are already rebuilt for ${target}`);
79
+ }
80
+ }).catch(errorOrSignal => {
81
+ if (typeof errorOrSignal === 'string' && errorOrSignal in os.constants.signals) {
82
+ process.kill(process.pid, errorOrSignal);
83
+ } else {
84
+ throw errorOrSignal;
85
+ }
86
+ });
87
+ }
88
+
89
+ function folderExists(folder: string): boolean {
90
+ if (fs.existsSync(folder)) {
91
+ if (fs.statSync(folder).isDirectory()) {
92
+ return true;
93
+ } else {
94
+ throw new Error(`"${folder}" exists but it is not a directory`);
95
+ }
96
+ }
97
+ return false;
98
+ }
99
+
100
+ /**
101
+ * Schema for `<browserModuleCache>/modules.json`.
102
+ */
103
+ interface ModulesJson {
104
+ [moduleName: string]: ModuleBackup
105
+ }
106
+ interface ModuleBackup {
107
+ originalLocation: string
108
+ }
109
+
110
+ async function rebuildElectronModules(browserModuleCache: string, modules: string[], forceAbi: NodeABI | undefined, token: ExitToken): Promise<number> {
111
+ const modulesJsonPath = path.join(browserModuleCache, 'modules.json');
112
+ const modulesJson: ModulesJson = await fs.access(modulesJsonPath).then(
113
+ () => fs.readJson(modulesJsonPath),
114
+ () => ({})
115
+ );
116
+ let success = true;
117
+ // Backup already built browser modules.
118
+ await Promise.all(modules.map(async module => {
119
+ let modulePath;
120
+ try {
121
+ modulePath = require.resolve(`${module}/package.json`, {
122
+ paths: [process.cwd()],
123
+ });
124
+ } catch (_) {
125
+ console.debug(`Module not found: ${module}`);
126
+ return; // Skip current module.
127
+ }
128
+ const src = path.dirname(modulePath);
129
+ const dest = path.join(browserModuleCache, module);
130
+ try {
131
+ await fs.remove(dest);
132
+ await fs.copy(src, dest, { overwrite: true });
133
+ modulesJson[module] = {
134
+ originalLocation: src,
135
+ };
136
+ console.debug(`Processed "${module}"`);
137
+ } catch (error) {
138
+ console.error(`Error while doing a backup for "${module}": ${error}`);
139
+ success = false;
140
+ }
141
+ }));
142
+ if (Object.keys(modulesJson).length === 0) {
143
+ console.debug('No module to rebuild.');
144
+ return 0;
145
+ }
146
+ // Update manifest tracking the backups' original locations.
147
+ await fs.writeJson(modulesJsonPath, modulesJson, { spaces: 2 });
148
+ // If we failed to process a module then exit now.
149
+ if (!success) {
150
+ return 1;
151
+ }
152
+ const todo = modules.map(m => {
153
+ // electron-rebuild ignores the module namespace...
154
+ const slash = m.indexOf('/');
155
+ return m.startsWith('@') && slash !== -1
156
+ ? m.substring(slash + 1)
157
+ : m;
158
+ });
159
+ let exitCode: number | undefined;
160
+ try {
161
+ if (process.env.THEIA_REBUILD_NO_WORKAROUND) {
162
+ exitCode = await runElectronRebuild(todo, forceAbi, token);
163
+ } else {
164
+ exitCode = await electronRebuildExtraModulesWorkaround(process.cwd(), todo, () => runElectronRebuild(todo, forceAbi, token), token);
165
+ }
166
+ } catch (error) {
167
+ console.error(error);
168
+ } finally {
169
+ // If code is undefined or different from zero we need to revert back to the browser modules.
170
+ if (exitCode !== 0) {
171
+ await revertBrowserModules(browserModuleCache, modules);
172
+ }
173
+ return exitCode ?? 1;
174
+ }
175
+ }
176
+
177
+ async function revertBrowserModules(browserModuleCache: string, modules: string[]): Promise<number> {
178
+ let exitCode = 0;
179
+ const modulesJsonPath = path.join(browserModuleCache, 'modules.json');
180
+ const modulesJson: ModulesJson = await fs.readJson(modulesJsonPath);
181
+ await Promise.all(Object.entries(modulesJson).map(async ([moduleName, entry]) => {
182
+ if (!modules.includes(moduleName)) {
183
+ return; // Skip modules that weren't requested.
184
+ }
185
+ const src = path.join(browserModuleCache, moduleName);
186
+ if (!await fs.pathExists(src)) {
187
+ delete modulesJson[moduleName];
188
+ console.error(`Missing backup for ${moduleName}!`);
189
+ exitCode = 1;
190
+ return;
191
+ }
192
+ const dest = entry.originalLocation;
193
+ try {
194
+ await fs.remove(dest);
195
+ await fs.copy(src, dest, { overwrite: false });
196
+ await fs.remove(src);
197
+ delete modulesJson[moduleName];
198
+ console.debug(`Reverted "${moduleName}"`);
199
+ } catch (error) {
200
+ console.error(`Error while reverting "${moduleName}": ${error}`);
201
+ exitCode = 1;
202
+ }
203
+ }));
204
+ if (Object.keys(modulesJson).length === 0) {
205
+ // We restored everything, so we can delete the cache.
206
+ await fs.remove(browserModuleCache);
207
+ } else {
208
+ // Some things were not restored, so we update the manifest.
209
+ await fs.writeJson(modulesJsonPath, modulesJson, { spaces: 2 });
210
+ }
211
+ return exitCode;
212
+ }
213
+
214
+ async function runElectronRebuild(modules: string[], forceAbi: NodeABI | undefined, token: ExitToken): Promise<number> {
215
+ const todo = modules.join(',');
216
+ return new Promise(async (resolve, reject) => {
217
+ let command = `npx --no-install electron-rebuild -f -w=${todo} -o=${todo}`;
218
+ if (forceAbi) {
219
+ command += ` --force-abi ${forceAbi}`;
220
+ }
221
+ const electronRebuild = cp.spawn(command, {
222
+ stdio: 'inherit',
223
+ shell: true,
224
+ });
225
+ token.onSignal(signal => electronRebuild.kill(signal));
226
+ electronRebuild.on('error', reject);
227
+ electronRebuild.on('close', (code, signal) => {
228
+ if (signal) {
229
+ reject(new Error(`electron-rebuild exited with "${signal}"`));
230
+ } else {
231
+ resolve(code!);
232
+ }
233
+ });
234
+ });
235
+ }
236
+
237
+ /**
238
+ * `electron-rebuild` is supposed to accept a list of modules to build, even when not part of the dependencies.
239
+ * But there is a bug that causes `electron-rebuild` to not correctly process this list of modules.
240
+ *
241
+ * This workaround will temporarily modify the current package.json file.
242
+ *
243
+ * PR with fix: https://github.com/electron/electron-rebuild/pull/888
244
+ *
245
+ * TODO: Remove this workaround.
246
+ */
247
+ async function electronRebuildExtraModulesWorkaround<T>(cwd: string, extraModules: string[], run: (token: ExitToken) => Promise<T>, token: ExitToken): Promise<T> {
248
+ const packageJsonPath = path.resolve(cwd, 'package.json');
249
+ if (await fs.pathExists(packageJsonPath)) {
250
+ // package.json exists: We back it up before modifying it then revert it.
251
+ const packageJsonCopyPath = `${packageJsonPath}.copy`;
252
+ const packageJson = await fs.readJson(packageJsonPath);
253
+ await fs.copy(packageJsonPath, packageJsonCopyPath);
254
+ await throwIfSignal(token, async () => {
255
+ await fs.unlink(packageJsonCopyPath);
256
+ });
257
+ if (typeof packageJson.dependencies !== 'object') {
258
+ packageJson.dependencies = {};
259
+ }
260
+ for (const extraModule of extraModules) {
261
+ if (!packageJson.dependencies[extraModule]) {
262
+ packageJson.dependencies[extraModule] = '*';
263
+ }
264
+ }
265
+ try {
266
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
267
+ await throwIfSignal(token);
268
+ return await run(token);
269
+ } finally {
270
+ await fs.move(packageJsonCopyPath, packageJsonPath, { overwrite: true });
271
+ }
272
+ } else {
273
+ // package.json does not exist: We create one then remove it.
274
+ const packageJson = {
275
+ name: 'theia-rebuild-workaround',
276
+ version: '0.0.0',
277
+ dependencies: {} as Record<string, string>,
278
+ };
279
+ for (const extraModule of extraModules) {
280
+ packageJson.dependencies[extraModule] = '*';
281
+ }
282
+ try {
283
+ await fs.writeJson(packageJsonPath, packageJson);
284
+ await throwIfSignal(token);
285
+ return await run(token);
286
+ } finally {
287
+ await fs.unlink(packageJsonPath);
288
+ }
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Temporarily install hooks to **try** to prevent the process from exiting while `run` is running.
294
+ *
295
+ * Note that it is still possible to kill the process and prevent cleanup logic (e.g. SIGKILL, computer forced shutdown, etc).
296
+ */
297
+ async function guardExit<T>(run: (token: ExitToken) => Promise<T>): Promise<T> {
298
+ const token = new ExitTokenImpl();
299
+ const signalListener = (signal: NodeJS.Signals) => token._emitSignal(signal);
300
+ for (const signal of EXIT_SIGNALS) {
301
+ process.on(signal, signalListener);
302
+ }
303
+ try {
304
+ return await run(token);
305
+ } finally {
306
+ for (const signal of EXIT_SIGNALS) {
307
+ process.off(signal, signalListener);
308
+ }
309
+ }
310
+ }
311
+
312
+ class ExitTokenImpl implements ExitToken {
313
+
314
+ protected _listeners = new Set<(signal: NodeJS.Signals) => void>();
315
+ protected _lastSignal?: NodeJS.Signals;
316
+
317
+ onSignal(callback: (signal: NodeJS.Signals) => void): void {
318
+ this._listeners.add(callback);
319
+ }
320
+
321
+ getLastSignal(): NodeJS.Signals | undefined {
322
+ return this._lastSignal;
323
+ }
324
+
325
+ _emitSignal(signal: NodeJS.Signals): void {
326
+ this._lastSignal = signal;
327
+ for (const listener of this._listeners) {
328
+ listener(signal);
329
+ }
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Throw `signal` if one was received, runs `cleanup` before doing so.
335
+ */
336
+ async function throwIfSignal(token: ExitToken, cleanup?: () => Promise<void>): Promise<void> {
337
+ if (token.getLastSignal()) {
338
+ try {
339
+ await cleanup?.();
340
+ } finally {
341
+ // eslint-disable-next-line no-throw-literal
342
+ throw token.getLastSignal()!;
343
+ }
344
+ }
345
+ }