@push.rocks/smartpuppeteer 2.2.0 → 2.4.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.
@@ -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
+ };
@@ -9,6 +9,9 @@ export type TLiveBrowserWaitUntil =
9
9
  | 'networkidle0'
10
10
  | 'networkidle2';
11
11
 
12
+ export const liveBrowserDefaultMaxOutstandingFrames = 3;
13
+ export const liveBrowserMaxOutstandingFrames = 64;
14
+
12
15
  export interface ILiveBrowserViewport {
13
16
  width: number;
14
17
  height: number;
@@ -21,6 +24,7 @@ export interface ILiveBrowserScreencastOptions {
21
24
  maxWidth?: number;
22
25
  maxHeight?: number;
23
26
  everyNthFrame?: number;
27
+ maxOutstandingFrames?: number;
24
28
  }
25
29
 
26
30
  export interface ILiveBrowserSecurityOptions {
@@ -28,6 +32,12 @@ export interface ILiveBrowserSecurityOptions {
28
32
  denyFileChoosers?: boolean;
29
33
  denyPermissions?: boolean;
30
34
  httpNavigationOnly?: boolean;
35
+ proxyCredentials?: ILiveBrowserProxyCredentials;
36
+ }
37
+
38
+ export interface ILiveBrowserProxyCredentials {
39
+ username: string;
40
+ password: string;
31
41
  }
32
42
 
33
43
  export interface ILiveBrowserOperationOptions {
@@ -66,6 +76,27 @@ export interface ILiveBrowserSessionOptions extends Omit<IEnvAwareOptions, 'laun
66
76
  allowEvaluation?: boolean;
67
77
  }
68
78
 
79
+ export interface ILiveBrowserProcessState {
80
+ generation: number;
81
+ pid: number | null;
82
+ processGroupId: number | null;
83
+ running: boolean;
84
+ exitCode: number | null;
85
+ signalCode: NodeJS.Signals | null;
86
+ }
87
+
88
+ export interface ILiveBrowserTerminationOptions {
89
+ gracefulTimeoutMs?: number;
90
+ forceTimeoutMs?: number;
91
+ }
92
+
93
+ export interface ILiveBrowserTerminationResult extends ILiveBrowserProcessState {
94
+ forced: boolean;
95
+ shutdownComplete: boolean;
96
+ confirmedDead: boolean;
97
+ errors: string[];
98
+ }
99
+
69
100
  export interface ILiveBrowserTabState {
70
101
  id: string;
71
102
  url: string;
@@ -1,8 +1,10 @@
1
1
  // node native scope
2
2
  import { Buffer } from 'node:buffer';
3
- import * as os from 'os';
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';