@push.rocks/smartpuppeteer 2.2.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 +31 -1
- package/dist_ts/smartpuppeteer.classes.livebrowsersession.js +798 -16
- 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 +23 -0
- package/dist_ts/smartpuppeteer.plugins.d.ts +4 -2
- package/dist_ts/smartpuppeteer.plugins.js +5 -3
- package/package.json +3 -3
- package/readme.md +15 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartpuppeteer.classes.livebrowsersession.ts +1016 -15
- package/ts/smartpuppeteer.helpers.process.ts +169 -0
- package/ts/smartpuppeteer.interfaces.livebrowser.ts +27 -0
- package/ts/smartpuppeteer.plugins.ts +4 -2
|
@@ -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
|
+
};
|
|
@@ -28,6 +28,12 @@ export interface ILiveBrowserSecurityOptions {
|
|
|
28
28
|
denyFileChoosers?: boolean;
|
|
29
29
|
denyPermissions?: boolean;
|
|
30
30
|
httpNavigationOnly?: boolean;
|
|
31
|
+
proxyCredentials?: ILiveBrowserProxyCredentials;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ILiveBrowserProxyCredentials {
|
|
35
|
+
username: string;
|
|
36
|
+
password: string;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export interface ILiveBrowserOperationOptions {
|
|
@@ -66,6 +72,27 @@ export interface ILiveBrowserSessionOptions extends Omit<IEnvAwareOptions, 'laun
|
|
|
66
72
|
allowEvaluation?: boolean;
|
|
67
73
|
}
|
|
68
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[];
|
|
94
|
+
}
|
|
95
|
+
|
|
69
96
|
export interface ILiveBrowserTabState {
|
|
70
97
|
id: string;
|
|
71
98
|
url: string;
|
|
@@ -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';
|