Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/puppet/lib/BrowserProcess.ts
1029 views
1
/**
2
* Copyright 2017 Google Inc. All rights reserved.
3
* Modifications copyright (c) Data Liberation Foundation Inc.
4
*
5
* Licensed under the Apache License, Version 2.0 (the "License");
6
* you may not use this file except in compliance with the License.
7
* You may obtain a copy of the License at
8
*
9
* http://www.apache.org/licenses/LICENSE-2.0
10
*
11
* Unless required by applicable law or agreed to in writing, software
12
* distributed under the License is distributed on an "AS IS" BASIS,
13
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
* See the License for the specific language governing permissions and
15
* limitations under the License.
16
*/
17
import * as childProcess from 'child_process';
18
import { ChildProcess } from 'child_process';
19
import * as readline from 'readline';
20
import Log from '@secret-agent/commons/Logger';
21
import * as Fs from 'fs';
22
import { TypedEventEmitter } from '@secret-agent/commons/eventUtils';
23
import IBrowserEngine from '@secret-agent/interfaces/IBrowserEngine';
24
import { bindFunctions } from '@secret-agent/commons/utils';
25
import { PipeTransport } from './PipeTransport';
26
27
const { log } = Log(module);
28
29
const logProcessExit = process.env.NODE_ENV !== 'test';
30
31
export default class BrowserProcess extends TypedEventEmitter<{ close: void }> {
32
public readonly transport: PipeTransport;
33
public hasLaunchError: Promise<Error>;
34
private processKilled = false;
35
private readonly launchedProcess: ChildProcess;
36
37
constructor(private browserEngine: IBrowserEngine, private env?: NodeJS.ProcessEnv) {
38
super();
39
40
bindFunctions(this);
41
this.launchedProcess = this.launch();
42
this.bindProcessEvents();
43
44
this.transport = new PipeTransport(this.launchedProcess);
45
this.bindCloseHandlers();
46
}
47
48
async close(): Promise<void> {
49
this.gracefulCloseBrowser();
50
await this.killChildProcess();
51
}
52
53
private bindCloseHandlers(): void {
54
process.once('exit', this.close);
55
process.once('uncaughtExceptionMonitor', this.close);
56
this.transport.onCloseFns.push(this.close);
57
}
58
59
private launch(): ChildProcess {
60
const { name, executablePath, launchArguments } = this.browserEngine;
61
log.info(`${name}.LaunchProcess`, { sessionId: null, executablePath, launchArguments });
62
63
return childProcess.spawn(executablePath, launchArguments, {
64
// On non-windows platforms, `detached: true` makes child process a
65
// leader of a new process group, making it possible to kill child
66
// process tree with `.kill(-pid)` command. @see
67
// https://nodejs.org/api/child_process.html#child_process_options_detached
68
detached: process.platform !== 'win32',
69
env: this.env,
70
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],
71
});
72
}
73
74
private bindProcessEvents(): void {
75
// Prevent Unhandled 'error' event.
76
let error: Error;
77
this.launchedProcess.on('error', e => {
78
error = e;
79
});
80
if (!this.launchedProcess.pid) {
81
this.hasLaunchError = new Promise<Error>(resolve => {
82
if (error) return resolve(error);
83
this.launchedProcess.once('error', err => {
84
resolve(new Error(`Failed to launch browser: ${err}`));
85
});
86
});
87
}
88
const { stdout, stderr } = this.launchedProcess;
89
const name = this.browserEngine.name;
90
91
readline.createInterface({ input: stdout }).on('line', line => {
92
if (line) log.stats(`${name}.stdout`, { message: line, sessionId: null });
93
});
94
readline.createInterface({ input: stderr }).on('line', line => {
95
if (line) log.warn(`${name}.stderr`, { message: line, sessionId: null });
96
});
97
98
this.launchedProcess.once('exit', this.onChildProcessExit);
99
}
100
101
private gracefulCloseBrowser(): void {
102
try {
103
// attempt graceful close, but don't wait
104
if (this.transport && !this.transport.isClosed) {
105
this.transport.send(JSON.stringify({ method: 'Browser.close', id: -1 }));
106
this.transport.close();
107
}
108
} catch (e) {
109
// this might fail, we want to keep going
110
}
111
}
112
113
private async killChildProcess(): Promise<void> {
114
const launchedProcess = this.launchedProcess;
115
try {
116
if (!launchedProcess.killed && !this.processKilled) {
117
const closed = new Promise<void>(resolve => launchedProcess.once('exit', resolve));
118
if (process.platform === 'win32') {
119
childProcess.execSync(`taskkill /pid ${launchedProcess.pid} /T /F 2> nul`);
120
} else {
121
launchedProcess.kill('SIGKILL');
122
}
123
launchedProcess.emit('exit');
124
await closed;
125
}
126
} catch (e) {
127
// might have already been kill off
128
}
129
}
130
131
private onChildProcessExit(exitCode: number, signal: NodeJS.Signals): void {
132
if (this.processKilled) return;
133
this.processKilled = true;
134
135
try {
136
this.transport?.close();
137
} catch (e) {
138
// drown
139
}
140
if (logProcessExit) {
141
const name = this.browserEngine.name;
142
log.stats(`${name}.ProcessExited`, { exitCode, signal, sessionId: null });
143
}
144
145
this.emit('close');
146
this.cleanDataDir();
147
}
148
149
private cleanDataDir(retries = 3): void {
150
const datadir = this.browserEngine.userDataDir;
151
if (!datadir) return;
152
try {
153
if (Fs.existsSync(datadir)) {
154
// rmdir is deprecated in node 16+
155
const fn = 'rmSync' in Fs ? 'rmSync' : 'rmdirSync';
156
Fs[fn](datadir, { recursive: true });
157
}
158
} catch (err) {
159
if (retries >= 0) {
160
this.cleanDataDir(retries - 1);
161
}
162
}
163
}
164
}
165
166