rechrome 1.31.7 → 1.31.8
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/package.json +1 -1
- package/serve.js +56 -37
- package/serve.ts +56 -37
package/package.json
CHANGED
package/serve.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readListeners, listenerAddress, authorizeProfileRequest, canReadProfileFile, profileOutputPrefix, normalizePrefix, resolveAllowedProfile, type Listener } from "./listeners.js";
|
|
2
2
|
import { file } from "bun";
|
|
3
3
|
import { createHash, X509Certificate } from "crypto";
|
|
4
|
-
import { mkdirSync, unlinkSync, accessSync, readdirSync, realpathSync, constants as fsConstants } from "fs";
|
|
4
|
+
import { mkdirSync, lstatSync, chmodSync, unlinkSync, accessSync, readdirSync, realpathSync, constants as fsConstants } from "fs";
|
|
5
5
|
import { join, resolve, relative, isAbsolute } from "path";
|
|
6
|
+
import { tmpdir } from "os";
|
|
6
7
|
import {
|
|
7
8
|
log,
|
|
8
9
|
parseUrl,
|
|
@@ -136,8 +137,42 @@ export function inferSilentExtensionFailure(options: {
|
|
|
136
137
|
return `Extension connection timeout after ${handshakeTimeoutMs}ms. Automatic recovery retry failed; reload the Playwright MCP Bridge extension at chrome://extensions and retry.\n`;
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
// Every CLI child must agree on its socket namespace, including probes and reapers.
|
|
141
|
+
export function playwrightCliEnv(
|
|
142
|
+
overrides: Record<string, string | undefined> = {},
|
|
143
|
+
runtime = { env: process.env, platform: process.platform, uid: process.getuid?.() },
|
|
144
|
+
): Record<string, string | undefined> {
|
|
145
|
+
const source = runtime.env;
|
|
146
|
+
const env: Record<string, string | undefined> = {
|
|
147
|
+
PATH: source.PATH,
|
|
148
|
+
HOME: source.HOME || HOME,
|
|
149
|
+
USERPROFILE: source.USERPROFILE,
|
|
150
|
+
TMPDIR: source.TMPDIR,
|
|
151
|
+
DISPLAY: source.DISPLAY,
|
|
152
|
+
XDG_RUNTIME_DIR: source.XDG_RUNTIME_DIR,
|
|
153
|
+
DEBUG: source.DEBUG,
|
|
154
|
+
PWDEBUG: source.PWDEBUG,
|
|
155
|
+
...overrides,
|
|
156
|
+
PLAYWRIGHT_SOCKETS_DIR: source.PLAYWRIGHT_SOCKETS_DIR,
|
|
157
|
+
};
|
|
158
|
+
if (runtime.platform === "darwin") {
|
|
159
|
+
if (runtime.uid === undefined) throw new Error("Cannot determine socket directory owner");
|
|
160
|
+
const dir = source.PLAYWRIGHT_SOCKETS_DIR || `/tmp/pw-${runtime.uid}`;
|
|
161
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
162
|
+
const stat = lstatSync(dir);
|
|
163
|
+
if (!stat.isDirectory() || stat.uid !== runtime.uid)
|
|
164
|
+
throw new Error(`Refusing unsafe Playwright socket directory: ${dir}`);
|
|
165
|
+
chmodSync(dir, 0o700);
|
|
166
|
+
env.PLAYWRIGHT_SOCKETS_DIR = dir;
|
|
167
|
+
}
|
|
168
|
+
return env;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function tmpSocketRoot(env = playwrightCliEnv()): string {
|
|
172
|
+
// Match the shipped coreBundle makeSocketPath('cli', session). CLI children do
|
|
173
|
+
// not inherit USER/USERNAME, so the vendor's fallback username is "default".
|
|
174
|
+
const userHash = createHash("sha1").update(env.USERNAME || env.USER || "default").digest("hex").slice(0, 8);
|
|
175
|
+
return join(env.PLAYWRIGHT_SOCKETS_DIR || join(env.TMPDIR || tmpdir(), `pw-${userHash}`), "cli");
|
|
141
176
|
}
|
|
142
177
|
|
|
143
178
|
// On startup, adopt any iso-* sessions a previous daemon left behind so they still get reaped
|
|
@@ -146,15 +181,11 @@ function tmpSocketRoot(): string {
|
|
|
146
181
|
function adoptOrphanedIsoSessions(): void {
|
|
147
182
|
try {
|
|
148
183
|
const root = tmpSocketRoot();
|
|
149
|
-
for (const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (m && !isoLastUsed.has(m[1])) {
|
|
155
|
-
isoLastUsed.set(m[1], Date.now());
|
|
156
|
-
log(`adopted orphaned isolated session for reaping: ${m[1]}`);
|
|
157
|
-
}
|
|
184
|
+
for (const f of readdirSync(root)) {
|
|
185
|
+
const sess = f.endsWith(".sock") ? f.slice(0, -5) : "";
|
|
186
|
+
if (isIsoSession(sess) && !isoLastUsed.has(sess)) {
|
|
187
|
+
isoLastUsed.set(sess, Date.now());
|
|
188
|
+
log(`adopted orphaned isolated session for reaping: ${sess}`);
|
|
158
189
|
}
|
|
159
190
|
}
|
|
160
191
|
} catch {
|
|
@@ -174,7 +205,7 @@ function reapIdleIsoSessions(bin: string, binArgs: string[], workDir: string): v
|
|
|
174
205
|
stdout: "ignore",
|
|
175
206
|
stderr: "ignore",
|
|
176
207
|
windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
|
|
177
|
-
env:
|
|
208
|
+
env: playwrightCliEnv(),
|
|
178
209
|
});
|
|
179
210
|
log(`reaped idle isolated session (idle ${Math.round((now - last) / 1000)}s): ${sess}`);
|
|
180
211
|
} catch (e) {
|
|
@@ -566,7 +597,7 @@ export async function serve() {
|
|
|
566
597
|
const probe = Bun.spawn([pbin, ...pbinArgs, "tab-list", `-s=${target}`], {
|
|
567
598
|
cwd: workDir, stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
568
599
|
windowsHide: true,
|
|
569
|
-
env:
|
|
600
|
+
env: playwrightCliEnv(),
|
|
570
601
|
});
|
|
571
602
|
const probeStatus = await Promise.race([
|
|
572
603
|
probe.exited,
|
|
@@ -735,7 +766,7 @@ export async function serve() {
|
|
|
735
766
|
stdout: "pipe",
|
|
736
767
|
stderr: "pipe",
|
|
737
768
|
windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
|
|
738
|
-
env:
|
|
769
|
+
env: playwrightCliEnv(),
|
|
739
770
|
});
|
|
740
771
|
const [listStatus, listOut] = await Promise.race([
|
|
741
772
|
Promise.all([listProc.exited, new Response(listProc.stdout).text()]),
|
|
@@ -764,14 +795,6 @@ export async function serve() {
|
|
|
764
795
|
}
|
|
765
796
|
|
|
766
797
|
const childEnv: Record<string, string | undefined> = {
|
|
767
|
-
PATH: process.env.PATH,
|
|
768
|
-
HOME: HOME,
|
|
769
|
-
USERPROFILE: process.env.USERPROFILE,
|
|
770
|
-
TMPDIR: process.env.TMPDIR,
|
|
771
|
-
DISPLAY: process.env.DISPLAY,
|
|
772
|
-
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
773
|
-
DEBUG: process.env.DEBUG, // forward debug namespaces (e.g. pw:mcp:relay) for diagnostics
|
|
774
|
-
PWDEBUG: process.env.PWDEBUG,
|
|
775
798
|
...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: shortClientLabel(clientName) } : {}),
|
|
776
799
|
...passthroughEnv,
|
|
777
800
|
// Enable extension bridge when credentials are present
|
|
@@ -779,19 +802,15 @@ export async function serve() {
|
|
|
779
802
|
? { PLAYWRIGHT_MCP_EXTENSION: "1" }
|
|
780
803
|
: {}),
|
|
781
804
|
};
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
const
|
|
805
|
+
// Only a remaining open needs cleanup; a successful probe converted it to
|
|
806
|
+
// goto and its live daemon socket must remain reachable.
|
|
807
|
+
if (filteredArgs[0] === "open") {
|
|
808
|
+
const socketRoot = tmpSocketRoot();
|
|
786
809
|
try {
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if (f.startsWith(namespacedSession)) {
|
|
792
|
-
const sockPath = `${subDir}/${f}`;
|
|
793
|
-
try { unlinkSync(sockPath); log(`Removed stale socket: ${sockPath}`); } catch {}
|
|
794
|
-
}
|
|
810
|
+
for (const f of readdirSync(socketRoot)) {
|
|
811
|
+
if (f === `${namespacedSession}.sock`) {
|
|
812
|
+
const sockPath = join(socketRoot, f);
|
|
813
|
+
try { unlinkSync(sockPath); log(`Removed stale socket: ${sockPath}`); } catch {}
|
|
795
814
|
}
|
|
796
815
|
}
|
|
797
816
|
} catch {}
|
|
@@ -804,7 +823,7 @@ export async function serve() {
|
|
|
804
823
|
stdout: "pipe",
|
|
805
824
|
stderr: "pipe",
|
|
806
825
|
windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
|
|
807
|
-
env: childEnv,
|
|
826
|
+
env: playwrightCliEnv(childEnv),
|
|
808
827
|
});
|
|
809
828
|
|
|
810
829
|
const TIMEOUT = 60_000;
|
|
@@ -860,7 +879,7 @@ export async function serve() {
|
|
|
860
879
|
Bun.spawn([bin, ...binArgs, "close", `-s=${namespacedSession}`], {
|
|
861
880
|
cwd: runWorkDir, stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
862
881
|
windowsHide: true,
|
|
863
|
-
env:
|
|
882
|
+
env: playwrightCliEnv(),
|
|
864
883
|
});
|
|
865
884
|
} catch {}
|
|
866
885
|
sessionTimeouts.delete(namespacedSession);
|
package/serve.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readListeners, listenerAddress, authorizeProfileRequest, canReadProfileFile, profileOutputPrefix, normalizePrefix, resolveAllowedProfile, type Listener } from "./listeners.ts";
|
|
2
2
|
import { file } from "bun";
|
|
3
3
|
import { createHash, X509Certificate } from "crypto";
|
|
4
|
-
import { mkdirSync, unlinkSync, accessSync, readdirSync, realpathSync, constants as fsConstants } from "fs";
|
|
4
|
+
import { mkdirSync, lstatSync, chmodSync, unlinkSync, accessSync, readdirSync, realpathSync, constants as fsConstants } from "fs";
|
|
5
5
|
import { join, resolve, relative, isAbsolute } from "path";
|
|
6
|
+
import { tmpdir } from "os";
|
|
6
7
|
import {
|
|
7
8
|
log,
|
|
8
9
|
parseUrl,
|
|
@@ -136,8 +137,42 @@ export function inferSilentExtensionFailure(options: {
|
|
|
136
137
|
return `Extension connection timeout after ${handshakeTimeoutMs}ms. Automatic recovery retry failed; reload the Playwright MCP Bridge extension at chrome://extensions and retry.\n`;
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
// Every CLI child must agree on its socket namespace, including probes and reapers.
|
|
141
|
+
export function playwrightCliEnv(
|
|
142
|
+
overrides: Record<string, string | undefined> = {},
|
|
143
|
+
runtime = { env: process.env, platform: process.platform, uid: process.getuid?.() },
|
|
144
|
+
): Record<string, string | undefined> {
|
|
145
|
+
const source = runtime.env;
|
|
146
|
+
const env: Record<string, string | undefined> = {
|
|
147
|
+
PATH: source.PATH,
|
|
148
|
+
HOME: source.HOME || HOME,
|
|
149
|
+
USERPROFILE: source.USERPROFILE,
|
|
150
|
+
TMPDIR: source.TMPDIR,
|
|
151
|
+
DISPLAY: source.DISPLAY,
|
|
152
|
+
XDG_RUNTIME_DIR: source.XDG_RUNTIME_DIR,
|
|
153
|
+
DEBUG: source.DEBUG,
|
|
154
|
+
PWDEBUG: source.PWDEBUG,
|
|
155
|
+
...overrides,
|
|
156
|
+
PLAYWRIGHT_SOCKETS_DIR: source.PLAYWRIGHT_SOCKETS_DIR,
|
|
157
|
+
};
|
|
158
|
+
if (runtime.platform === "darwin") {
|
|
159
|
+
if (runtime.uid === undefined) throw new Error("Cannot determine socket directory owner");
|
|
160
|
+
const dir = source.PLAYWRIGHT_SOCKETS_DIR || `/tmp/pw-${runtime.uid}`;
|
|
161
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
162
|
+
const stat = lstatSync(dir);
|
|
163
|
+
if (!stat.isDirectory() || stat.uid !== runtime.uid)
|
|
164
|
+
throw new Error(`Refusing unsafe Playwright socket directory: ${dir}`);
|
|
165
|
+
chmodSync(dir, 0o700);
|
|
166
|
+
env.PLAYWRIGHT_SOCKETS_DIR = dir;
|
|
167
|
+
}
|
|
168
|
+
return env;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function tmpSocketRoot(env = playwrightCliEnv()): string {
|
|
172
|
+
// Match the shipped coreBundle makeSocketPath('cli', session). CLI children do
|
|
173
|
+
// not inherit USER/USERNAME, so the vendor's fallback username is "default".
|
|
174
|
+
const userHash = createHash("sha1").update(env.USERNAME || env.USER || "default").digest("hex").slice(0, 8);
|
|
175
|
+
return join(env.PLAYWRIGHT_SOCKETS_DIR || join(env.TMPDIR || tmpdir(), `pw-${userHash}`), "cli");
|
|
141
176
|
}
|
|
142
177
|
|
|
143
178
|
// On startup, adopt any iso-* sessions a previous daemon left behind so they still get reaped
|
|
@@ -146,15 +181,11 @@ function tmpSocketRoot(): string {
|
|
|
146
181
|
function adoptOrphanedIsoSessions(): void {
|
|
147
182
|
try {
|
|
148
183
|
const root = tmpSocketRoot();
|
|
149
|
-
for (const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (m && !isoLastUsed.has(m[1])) {
|
|
155
|
-
isoLastUsed.set(m[1], Date.now());
|
|
156
|
-
log(`adopted orphaned isolated session for reaping: ${m[1]}`);
|
|
157
|
-
}
|
|
184
|
+
for (const f of readdirSync(root)) {
|
|
185
|
+
const sess = f.endsWith(".sock") ? f.slice(0, -5) : "";
|
|
186
|
+
if (isIsoSession(sess) && !isoLastUsed.has(sess)) {
|
|
187
|
+
isoLastUsed.set(sess, Date.now());
|
|
188
|
+
log(`adopted orphaned isolated session for reaping: ${sess}`);
|
|
158
189
|
}
|
|
159
190
|
}
|
|
160
191
|
} catch {
|
|
@@ -174,7 +205,7 @@ function reapIdleIsoSessions(bin: string, binArgs: string[], workDir: string): v
|
|
|
174
205
|
stdout: "ignore",
|
|
175
206
|
stderr: "ignore",
|
|
176
207
|
windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
|
|
177
|
-
env:
|
|
208
|
+
env: playwrightCliEnv(),
|
|
178
209
|
});
|
|
179
210
|
log(`reaped idle isolated session (idle ${Math.round((now - last) / 1000)}s): ${sess}`);
|
|
180
211
|
} catch (e) {
|
|
@@ -566,7 +597,7 @@ export async function serve() {
|
|
|
566
597
|
const probe = Bun.spawn([pbin, ...pbinArgs, "tab-list", `-s=${target}`], {
|
|
567
598
|
cwd: workDir, stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
568
599
|
windowsHide: true,
|
|
569
|
-
env:
|
|
600
|
+
env: playwrightCliEnv(),
|
|
570
601
|
});
|
|
571
602
|
const probeStatus = await Promise.race([
|
|
572
603
|
probe.exited,
|
|
@@ -735,7 +766,7 @@ export async function serve() {
|
|
|
735
766
|
stdout: "pipe",
|
|
736
767
|
stderr: "pipe",
|
|
737
768
|
windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
|
|
738
|
-
env:
|
|
769
|
+
env: playwrightCliEnv(),
|
|
739
770
|
});
|
|
740
771
|
const [listStatus, listOut] = await Promise.race([
|
|
741
772
|
Promise.all([listProc.exited, new Response(listProc.stdout).text()]),
|
|
@@ -764,14 +795,6 @@ export async function serve() {
|
|
|
764
795
|
}
|
|
765
796
|
|
|
766
797
|
const childEnv: Record<string, string | undefined> = {
|
|
767
|
-
PATH: process.env.PATH,
|
|
768
|
-
HOME: HOME,
|
|
769
|
-
USERPROFILE: process.env.USERPROFILE,
|
|
770
|
-
TMPDIR: process.env.TMPDIR,
|
|
771
|
-
DISPLAY: process.env.DISPLAY,
|
|
772
|
-
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
773
|
-
DEBUG: process.env.DEBUG, // forward debug namespaces (e.g. pw:mcp:relay) for diagnostics
|
|
774
|
-
PWDEBUG: process.env.PWDEBUG,
|
|
775
798
|
...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: shortClientLabel(clientName) } : {}),
|
|
776
799
|
...passthroughEnv,
|
|
777
800
|
// Enable extension bridge when credentials are present
|
|
@@ -779,19 +802,15 @@ export async function serve() {
|
|
|
779
802
|
? { PLAYWRIGHT_MCP_EXTENSION: "1" }
|
|
780
803
|
: {}),
|
|
781
804
|
};
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
const
|
|
805
|
+
// Only a remaining open needs cleanup; a successful probe converted it to
|
|
806
|
+
// goto and its live daemon socket must remain reachable.
|
|
807
|
+
if (filteredArgs[0] === "open") {
|
|
808
|
+
const socketRoot = tmpSocketRoot();
|
|
786
809
|
try {
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
if (f.startsWith(namespacedSession)) {
|
|
792
|
-
const sockPath = `${subDir}/${f}`;
|
|
793
|
-
try { unlinkSync(sockPath); log(`Removed stale socket: ${sockPath}`); } catch {}
|
|
794
|
-
}
|
|
810
|
+
for (const f of readdirSync(socketRoot)) {
|
|
811
|
+
if (f === `${namespacedSession}.sock`) {
|
|
812
|
+
const sockPath = join(socketRoot, f);
|
|
813
|
+
try { unlinkSync(sockPath); log(`Removed stale socket: ${sockPath}`); } catch {}
|
|
795
814
|
}
|
|
796
815
|
}
|
|
797
816
|
} catch {}
|
|
@@ -804,7 +823,7 @@ export async function serve() {
|
|
|
804
823
|
stdout: "pipe",
|
|
805
824
|
stderr: "pipe",
|
|
806
825
|
windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
|
|
807
|
-
env: childEnv,
|
|
826
|
+
env: playwrightCliEnv(childEnv),
|
|
808
827
|
});
|
|
809
828
|
|
|
810
829
|
const TIMEOUT = 60_000;
|
|
@@ -860,7 +879,7 @@ export async function serve() {
|
|
|
860
879
|
Bun.spawn([bin, ...binArgs, "close", `-s=${namespacedSession}`], {
|
|
861
880
|
cwd: runWorkDir, stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
862
881
|
windowsHide: true,
|
|
863
|
-
env:
|
|
882
|
+
env: playwrightCliEnv(),
|
|
864
883
|
});
|
|
865
884
|
} catch {}
|
|
866
885
|
sessionTimeouts.delete(namespacedSession);
|