@push.rocks/smartpuppeteer 2.1.0 → 2.3.0
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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.d.ts +53 -15
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.js +1404 -60
- package/dist_ts/smartpuppeteer.classes.smartpuppeteer.d.ts +1 -0
- package/dist_ts/smartpuppeteer.classes.smartpuppeteer.js +23 -4
- package/dist_ts/smartpuppeteer.helpers.process.d.ts +14 -0
- package/dist_ts/smartpuppeteer.helpers.process.js +132 -0
- package/dist_ts/smartpuppeteer.interfaces.livebrowser.d.ts +47 -0
- package/dist_ts/smartpuppeteer.plugins.d.ts +4 -2
- package/dist_ts/smartpuppeteer.plugins.js +5 -3
- package/package.json +4 -4
- package/readme.md +52 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartpuppeteer.classes.livebrowsersession.ts +1768 -90
- package/ts/smartpuppeteer.classes.smartpuppeteer.ts +27 -4
- package/ts/smartpuppeteer.helpers.process.ts +169 -0
- package/ts/smartpuppeteer.interfaces.livebrowser.ts +59 -0
- package/ts/smartpuppeteer.plugins.ts +4 -2
|
@@ -2,10 +2,18 @@ import * as plugins from './smartpuppeteer.plugins.js';
|
|
|
2
2
|
|
|
3
3
|
export interface IEnvAwareOptions {
|
|
4
4
|
forceNoSandbox?: boolean;
|
|
5
|
+
requireSandbox?: boolean;
|
|
5
6
|
usePipe?: boolean;
|
|
6
7
|
launchOptions?: plugins.puppeteer.LaunchOptions;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
const sandboxDisablingArguments = [
|
|
11
|
+
'--disable-gpu-sandbox',
|
|
12
|
+
'--disable-seccomp-filter-sandbox',
|
|
13
|
+
'--disable-setuid-sandbox',
|
|
14
|
+
'--no-sandbox',
|
|
15
|
+
];
|
|
16
|
+
|
|
9
17
|
export const resolveBrowserExecutablePath = (
|
|
10
18
|
candidateNamesArg: string[] = [
|
|
11
19
|
'google-chrome',
|
|
@@ -29,15 +37,30 @@ export const getEnvAwareBrowserInstance = async (
|
|
|
29
37
|
): Promise<plugins.puppeteer.Browser> => {
|
|
30
38
|
const options: IEnvAwareOptions = {
|
|
31
39
|
forceNoSandbox: false,
|
|
40
|
+
requireSandbox: false,
|
|
32
41
|
...optionsArg,
|
|
33
42
|
};
|
|
34
43
|
|
|
44
|
+
if (options.forceNoSandbox && options.requireSandbox) {
|
|
45
|
+
throw new Error('forceNoSandbox and requireSandbox are mutually exclusive');
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
const launchOptions = options.launchOptions ?? {};
|
|
36
49
|
let chromeArgs: string[] = [...(launchOptions.args ?? [])];
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
if (options.requireSandbox) {
|
|
51
|
+
const forbiddenArgument = chromeArgs.find((argument) => (
|
|
52
|
+
sandboxDisablingArguments.includes(argument.split('=', 1)[0]!)
|
|
53
|
+
));
|
|
54
|
+
if (forbiddenArgument) {
|
|
55
|
+
throw new Error(`Sandbox-required browser launch rejects argument: ${forbiddenArgument}`);
|
|
56
|
+
}
|
|
57
|
+
if (process.platform === 'linux' && process.getuid?.() === 0) {
|
|
58
|
+
throw new Error('Sandbox-required Chromium cannot be launched as root');
|
|
59
|
+
}
|
|
60
|
+
} else if (
|
|
61
|
+
process.env.CI
|
|
62
|
+
|| options.forceNoSandbox
|
|
63
|
+
|| plugins.os.userInfo().username === 'root'
|
|
41
64
|
) {
|
|
42
65
|
for (const sandboxArg of ['--no-sandbox', '--disable-setuid-sandbox']) {
|
|
43
66
|
if (!chromeArgs.includes(sandboxArg)) {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as plugins from './smartpuppeteer.plugins.js';
|
|
2
|
+
|
|
3
|
+
export interface IOwnedProcessIdentity {
|
|
4
|
+
pid: number;
|
|
5
|
+
parentPid: number;
|
|
6
|
+
processGroupId: number;
|
|
7
|
+
sessionId: number;
|
|
8
|
+
startTime: string;
|
|
9
|
+
state: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const maxOwnedGroupProcesses = 4096;
|
|
13
|
+
const maxVisibleProcesses = 65536;
|
|
14
|
+
const processReadBatchSize = 64;
|
|
15
|
+
|
|
16
|
+
const parseProcessStat = (pid: number, stat: string): IOwnedProcessIdentity => {
|
|
17
|
+
const commandEnd = stat.lastIndexOf(')');
|
|
18
|
+
if (commandEnd < 0) {
|
|
19
|
+
throw new Error(`Unable to parse process identity for PID ${pid}`);
|
|
20
|
+
}
|
|
21
|
+
const fields = stat.slice(commandEnd + 2).trim().split(/\s+/);
|
|
22
|
+
const state = fields[0];
|
|
23
|
+
const parentPid = Number(fields[1]);
|
|
24
|
+
const processGroupId = Number(fields[2]);
|
|
25
|
+
const sessionId = Number(fields[3]);
|
|
26
|
+
const startTime = fields[19];
|
|
27
|
+
if (
|
|
28
|
+
typeof state !== 'string'
|
|
29
|
+
|| state.length !== 1
|
|
30
|
+
|| !Number.isInteger(parentPid)
|
|
31
|
+
|| parentPid < 0
|
|
32
|
+
|| !Number.isInteger(processGroupId)
|
|
33
|
+
|| processGroupId < 0
|
|
34
|
+
|| !Number.isInteger(sessionId)
|
|
35
|
+
|| sessionId < 0
|
|
36
|
+
|| typeof startTime !== 'string'
|
|
37
|
+
|| !/^[0-9]+$/.test(startTime)
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(`Unable to parse process identity for PID ${pid}`);
|
|
40
|
+
}
|
|
41
|
+
return { pid, parentPid, processGroupId, sessionId, startTime, state };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const readOwnedProcessIdentity = async (
|
|
45
|
+
pid: number,
|
|
46
|
+
): Promise<IOwnedProcessIdentity | undefined> => {
|
|
47
|
+
if (process.platform !== 'linux') {
|
|
48
|
+
throw new Error('Confirmed browser process ownership is supported on Linux only');
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const stat = await plugins.fs.promises.readFile(`/proc/${pid}/stat`, 'utf8');
|
|
52
|
+
return parseProcessStat(pid, stat);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (
|
|
55
|
+
(error as NodeJS.ErrnoException).code === 'ENOENT'
|
|
56
|
+
|| (error as NodeJS.ErrnoException).code === 'ESRCH'
|
|
57
|
+
) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const listProcessIdentities = async (): Promise<IOwnedProcessIdentity[]> => {
|
|
65
|
+
const entries = await plugins.fs.promises.readdir('/proc', { withFileTypes: true });
|
|
66
|
+
const pids = entries
|
|
67
|
+
.filter((entry) => entry.isDirectory() && /^[0-9]+$/.test(entry.name))
|
|
68
|
+
.map((entry) => Number(entry.name));
|
|
69
|
+
if (pids.length > maxVisibleProcesses) {
|
|
70
|
+
throw new Error(`Refusing to inspect more than ${maxVisibleProcesses} processes`);
|
|
71
|
+
}
|
|
72
|
+
const identities: Array<IOwnedProcessIdentity | undefined> = [];
|
|
73
|
+
for (let index = 0; index < pids.length; index += processReadBatchSize) {
|
|
74
|
+
identities.push(...await Promise.all(
|
|
75
|
+
pids.slice(index, index + processReadBatchSize).map((pid) => readOwnedProcessIdentity(pid)),
|
|
76
|
+
));
|
|
77
|
+
}
|
|
78
|
+
return identities.filter((identity): identity is IOwnedProcessIdentity => Boolean(identity));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const listOwnedProcessGroupMembers = async (
|
|
82
|
+
rootIdentity: IOwnedProcessIdentity,
|
|
83
|
+
): Promise<IOwnedProcessIdentity[]> => {
|
|
84
|
+
const currentRoot = await readOwnedProcessIdentity(rootIdentity.pid);
|
|
85
|
+
if (currentRoot && currentRoot.startTime !== rootIdentity.startTime) {
|
|
86
|
+
throw new Error('The owned browser PID was reused by another process');
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
currentRoot
|
|
90
|
+
&& (
|
|
91
|
+
currentRoot.processGroupId !== rootIdentity.processGroupId
|
|
92
|
+
|| currentRoot.sessionId !== rootIdentity.sessionId
|
|
93
|
+
)
|
|
94
|
+
) {
|
|
95
|
+
throw new Error('The owned browser process changed its process-group identity');
|
|
96
|
+
}
|
|
97
|
+
const members = (await listProcessIdentities()).filter((identity) => (
|
|
98
|
+
identity.processGroupId === rootIdentity.processGroupId
|
|
99
|
+
&& identity.sessionId === rootIdentity.sessionId
|
|
100
|
+
));
|
|
101
|
+
if (members.length > maxOwnedGroupProcesses) {
|
|
102
|
+
throw new Error(`Owned browser process group exceeded ${maxOwnedGroupProcesses} processes`);
|
|
103
|
+
}
|
|
104
|
+
return members;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const killFrozenOwnedProcessGroup = (rootIdentity: IOwnedProcessIdentity): boolean => {
|
|
108
|
+
try {
|
|
109
|
+
process.kill(-rootIdentity.processGroupId, 'SIGKILL');
|
|
110
|
+
return true;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if ((error as NodeJS.ErrnoException).code === 'ESRCH') {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const signalOwnedProcessGroup = async (
|
|
120
|
+
rootIdentity: IOwnedProcessIdentity,
|
|
121
|
+
signal: NodeJS.Signals,
|
|
122
|
+
): Promise<boolean> => {
|
|
123
|
+
const currentRoot = await readOwnedProcessIdentity(rootIdentity.pid);
|
|
124
|
+
if (!currentRoot) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
currentRoot.startTime !== rootIdentity.startTime
|
|
129
|
+
|| currentRoot.processGroupId !== rootIdentity.processGroupId
|
|
130
|
+
|| currentRoot.sessionId !== rootIdentity.sessionId
|
|
131
|
+
) {
|
|
132
|
+
throw new Error('Refusing to signal a reused browser process-group leader');
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
process.kill(-rootIdentity.processGroupId, signal);
|
|
136
|
+
return true;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if ((error as NodeJS.ErrnoException).code === 'ESRCH') {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const signalOwnedProcessIdentity = async (
|
|
146
|
+
identity: IOwnedProcessIdentity,
|
|
147
|
+
signal: NodeJS.Signals,
|
|
148
|
+
): Promise<boolean> => {
|
|
149
|
+
const currentIdentity = await readOwnedProcessIdentity(identity.pid);
|
|
150
|
+
if (!currentIdentity) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (currentIdentity.startTime !== identity.startTime) {
|
|
154
|
+
throw new Error(`Refusing to signal reused PID ${identity.pid}`);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
process.kill(identity.pid, signal);
|
|
158
|
+
return true;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if ((error as NodeJS.ErrnoException).code === 'ESRCH') {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const delay = async (milliseconds: number): Promise<void> => {
|
|
168
|
+
await new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
|
|
169
|
+
};
|
|
@@ -23,6 +23,42 @@ export interface ILiveBrowserScreencastOptions {
|
|
|
23
23
|
everyNthFrame?: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface ILiveBrowserSecurityOptions {
|
|
27
|
+
denyDownloads?: boolean;
|
|
28
|
+
denyFileChoosers?: boolean;
|
|
29
|
+
denyPermissions?: boolean;
|
|
30
|
+
httpNavigationOnly?: boolean;
|
|
31
|
+
proxyCredentials?: ILiveBrowserProxyCredentials;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ILiveBrowserProxyCredentials {
|
|
35
|
+
username: string;
|
|
36
|
+
password: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ILiveBrowserOperationOptions {
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type TLiveBrowserJsonValue =
|
|
44
|
+
| null
|
|
45
|
+
| boolean
|
|
46
|
+
| number
|
|
47
|
+
| string
|
|
48
|
+
| TLiveBrowserJsonValue[]
|
|
49
|
+
| { [key: string]: TLiveBrowserJsonValue };
|
|
50
|
+
|
|
51
|
+
export interface ILiveBrowserEvaluateOptions {
|
|
52
|
+
tabId?: string;
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
maxOutputBytes?: number;
|
|
55
|
+
maxDepth?: number;
|
|
56
|
+
maxNodes?: number;
|
|
57
|
+
maxStringBytes?: number;
|
|
58
|
+
maxArrayLength?: number;
|
|
59
|
+
maxObjectKeys?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
26
62
|
export type TLiveBrowserLaunchOptions = Omit<
|
|
27
63
|
NonNullable<IEnvAwareOptions['launchOptions']>,
|
|
28
64
|
'signal'
|
|
@@ -32,6 +68,29 @@ export interface ILiveBrowserSessionOptions extends Omit<IEnvAwareOptions, 'laun
|
|
|
32
68
|
launchOptions?: TLiveBrowserLaunchOptions;
|
|
33
69
|
viewport?: ILiveBrowserViewport;
|
|
34
70
|
screencast?: ILiveBrowserScreencastOptions;
|
|
71
|
+
security?: ILiveBrowserSecurityOptions;
|
|
72
|
+
allowEvaluation?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ILiveBrowserProcessState {
|
|
76
|
+
generation: number;
|
|
77
|
+
pid: number | null;
|
|
78
|
+
processGroupId: number | null;
|
|
79
|
+
running: boolean;
|
|
80
|
+
exitCode: number | null;
|
|
81
|
+
signalCode: NodeJS.Signals | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ILiveBrowserTerminationOptions {
|
|
85
|
+
gracefulTimeoutMs?: number;
|
|
86
|
+
forceTimeoutMs?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ILiveBrowserTerminationResult extends ILiveBrowserProcessState {
|
|
90
|
+
forced: boolean;
|
|
91
|
+
shutdownComplete: boolean;
|
|
92
|
+
confirmedDead: boolean;
|
|
93
|
+
errors: string[];
|
|
35
94
|
}
|
|
36
95
|
|
|
37
96
|
export interface ILiveBrowserTabState {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// node native scope
|
|
2
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
-
import * as
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as http from 'node:http';
|
|
5
|
+
import * as os from 'node:os';
|
|
4
6
|
|
|
5
|
-
export { Buffer, os };
|
|
7
|
+
export { Buffer, fs, http, os };
|
|
6
8
|
|
|
7
9
|
// @pushrocks scope
|
|
8
10
|
import * as smartdelay from '@push.rocks/smartdelay';
|