Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/replay/index.ts
1028 views
1
import * as ChildProcess from 'child_process';
2
import * as Fs from 'fs';
3
import * as Http from 'http';
4
import * as Lockfile from 'proper-lockfile';
5
import {
6
getBinaryPath,
7
getInstallDirectory,
8
getLocalBuildPath,
9
isBinaryInstalled,
10
isLocalBuildPresent,
11
} from './install/Utils';
12
13
const replayDir = getInstallDirectory();
14
const registrationApiFilepath = `${replayDir}/api.txt`;
15
const launchLockPath = `${replayDir}/launch`;
16
17
let registrationHost = '';
18
function resolveHost() {
19
try {
20
registrationHost = Fs.readFileSync(registrationApiFilepath, 'utf8');
21
} catch (err) {
22
// no-op
23
registrationHost = '';
24
}
25
return registrationHost;
26
}
27
28
try {
29
if (!Fs.existsSync(replayDir)) {
30
Fs.mkdirSync(replayDir, { recursive: true });
31
}
32
if (!Fs.existsSync(registrationApiFilepath)) {
33
Fs.writeFileSync(registrationApiFilepath, '');
34
} else {
35
resolveHost();
36
}
37
} catch (err) {
38
// couldn't listen for file
39
}
40
41
let apiStartPath: string;
42
try {
43
apiStartPath = require.resolve('@secret-agent/core/start');
44
} catch (err) {
45
// not installed locally (not full-client)
46
}
47
48
let hasLocalReplay = false;
49
try {
50
require.resolve('./app');
51
hasLocalReplay = Boolean(JSON.parse(process.env.SA_USE_REPLAY_BINARY ?? 'false')) === false;
52
} catch (err) {
53
// not installed locally
54
}
55
56
const showDebugLogs = Boolean(JSON.parse(process.env.SA_REPLAY_DEBUG ?? 'false'));
57
58
export async function replay(launchArgs: IReplayScriptRegistration): Promise<any> {
59
const {
60
replayApiUrl,
61
sessionsDataLocation,
62
sessionName,
63
scriptInstanceId,
64
sessionId,
65
scriptStartDate,
66
} = launchArgs;
67
68
const scriptMeta = {
69
replayApiUrl,
70
dataLocation: sessionsDataLocation,
71
sessionName,
72
sessionId,
73
scriptStartDate,
74
scriptInstanceId,
75
apiStartPath,
76
nodePath: process.execPath,
77
};
78
79
if (await registerScript(scriptMeta)) {
80
return;
81
}
82
83
// cross-process lock around the launch process so we don't open multiple instances
84
let release: () => Promise<void>;
85
try {
86
release = await Lockfile.lock(launchLockPath, {
87
retries: 5,
88
stale: 30e3,
89
fs: Fs,
90
realpath: false,
91
});
92
// make sure last "lock holder" didn't write the
93
if (await registerScript(scriptMeta)) {
94
return;
95
}
96
97
await openReplayApp('--sa-replay');
98
99
if (!(await registerScript(scriptMeta))) {
100
console.log("Couldn't register this script with the Replay app.", scriptMeta);
101
}
102
} catch (err) {
103
if (showDebugLogs) {
104
console.log('Error launching Replay', scriptMeta, err);
105
}
106
} finally {
107
if (release) await release();
108
}
109
}
110
111
export async function openReplayApp(...extraArgs: string[]) {
112
Fs.writeFileSync(registrationApiFilepath, '');
113
114
let child: ChildProcess.ChildProcess;
115
if (isLocalBuildPresent()) {
116
child = await launchReplay(getLocalBuildPath(), ['--local-build-launch', ...extraArgs]);
117
} else if (hasLocalReplay) {
118
const replayPath = require.resolve('@secret-agent/replay');
119
child = await launchReplay(
120
'yarn electron',
121
[replayPath, '--electron-launch', ...extraArgs],
122
true,
123
);
124
} else if (isBinaryInstalled()) {
125
child = await launchReplay(getBinaryPath(), ['--binary-launch', ...extraArgs]);
126
}
127
128
let registrationResolve: () => any;
129
let registrationReject: (reason?: any) => any;
130
const registrationComplete = new Promise<void>((resolve, reject) => {
131
registrationResolve = resolve;
132
registrationReject = reject;
133
});
134
const onPrematureExit = (code, signal) =>
135
registrationReject(new Error(`Replay shutdown with exit code ${code}: ${signal}`));
136
child.once('exit', onPrematureExit);
137
child.once('error', registrationReject);
138
// wait for change
139
const watcher = Fs.watch(registrationApiFilepath, { persistent: false, recursive: false }, () => {
140
if (resolveHost()) {
141
registrationResolve();
142
}
143
});
144
return registrationComplete.finally(() => {
145
watcher.close();
146
child.off('exit', onPrematureExit);
147
child.off('error', registrationReject);
148
});
149
}
150
151
function launchReplay(
152
appPath: string,
153
args: string[],
154
needsShell = false,
155
): ChildProcess.ChildProcess {
156
const child = ChildProcess.spawn(appPath, args, {
157
detached: true,
158
stdio: ['ignore', showDebugLogs ? 'inherit' : 'ignore', showDebugLogs ? 'inherit' : 'ignore'],
159
shell: needsShell,
160
windowsHide: false,
161
});
162
child.unref();
163
return child;
164
}
165
166
async function registerScript(data: any): Promise<boolean> {
167
if (!resolveHost()) return false;
168
169
try {
170
const url = new URL(registrationHost);
171
const request = Http.request(url, {
172
method: 'POST',
173
headers: { 'Content-Type': 'application/json' },
174
});
175
const response = new Promise<Http.IncomingMessage>((resolve, reject) => {
176
request.on('error', reject);
177
request.on('response', resolve);
178
});
179
request.end(JSON.stringify(data));
180
if ((await response)?.statusCode === 200) {
181
// registered successfully
182
return true;
183
}
184
} catch (err) {
185
// doesn't exist
186
}
187
Fs.writeFileSync(registrationApiFilepath, '');
188
registrationHost = '';
189
return false;
190
}
191
192
interface IReplayScriptRegistration {
193
replayApiUrl: string;
194
sessionsDataLocation: string;
195
sessionName: string;
196
sessionId: string;
197
scriptStartDate: string;
198
scriptInstanceId: string;
199
}
200
201