Skip to content

Commit a8c8fe9

Browse files
committed
Fix launcher sandbox defaults
1 parent 965fe7f commit a8c8fe9

9 files changed

Lines changed: 85 additions & 65 deletions

File tree

go/modcdp/launcher/LocalBrowserLauncher.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,11 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser,
102102
if headless {
103103
args = append(args, "--headless=new")
104104
}
105-
if options.Sandbox == nil || !*options.Sandbox {
105+
sandbox := runtime.GOOS != "linux"
106+
if options.Sandbox != nil {
107+
sandbox = *options.Sandbox
108+
}
109+
if !sandbox {
106110
args = append(args, "--no-sandbox")
107111
}
108112
args = append(args, options.Args...)

go/modcdp/launcher/LocalBrowserLauncher_test.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"os"
7+
"runtime"
78
"strconv"
89
"strings"
910
"testing"
@@ -23,7 +24,7 @@ func TestLocalBrowserLauncherClassHelpersMatchLocalLauncherSurface(t *testing.T)
2324
}
2425
}
2526

26-
func TestLocalBrowserLauncherLaunchesRealBrowserAndSpeaksCDP(t *testing.T) {
27+
func TestLocalBrowserLauncherLaunchesRealBrowserOverChosenCDPPortAndHonorsLaunchOptions(t *testing.T) {
2728
headless := true
2829
profileDir := t.TempDir()
2930
port, err := freePort()
@@ -101,6 +102,36 @@ func TestLocalBrowserLauncherLaunchesRealBrowserAndSpeaksCDP(t *testing.T) {
101102
if response.Result.ProtocolVersion == "" {
102103
t.Fatal("expected protocolVersion")
103104
}
105+
if err := wsutil.WriteClientText(conn, []byte(`{"id":2,"method":"SystemInfo.getInfo","params":{}}`)); err != nil {
106+
t.Fatal(err)
107+
}
108+
systemInfoBody, err := wsutil.ReadServerText(conn)
109+
if err != nil {
110+
t.Fatal(err)
111+
}
112+
var systemInfoResponse struct {
113+
ID int `json:"id"`
114+
Result struct {
115+
CommandLine string `json:"commandLine"`
116+
} `json:"result"`
117+
}
118+
if err := json.Unmarshal(systemInfoBody, &systemInfoResponse); err != nil {
119+
t.Fatal(err)
120+
}
121+
if systemInfoResponse.ID != 2 {
122+
t.Fatalf("unexpected SystemInfo.getInfo response id %d", systemInfoResponse.ID)
123+
}
124+
command := systemInfoResponse.Result.CommandLine
125+
if !strings.Contains(command, "--window-size=900,700") {
126+
t.Fatalf("expected browser command to include --window-size=900,700: %s", command)
127+
}
128+
if runtime.GOOS == "linux" {
129+
if !strings.Contains(command, "--no-sandbox") {
130+
t.Fatalf("expected Linux browser command to include --no-sandbox: %s", command)
131+
}
132+
} else if strings.Contains(command, "--no-sandbox") {
133+
t.Fatalf("expected browser command not to include --no-sandbox: %s", command)
134+
}
104135
}
105136

106137
func TestLocalBrowserLauncherLaunchesRealBrowserOverRemoteDebuggingPipe(t *testing.T) {

js/src/launcher/LocalBrowserLauncher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ export class LocalBrowserLauncher extends BrowserLauncher {
261261
port,
262262
user_data_dir,
263263
headless = process.platform === "linux" && !process.env.DISPLAY,
264-
sandbox = false,
264+
sandbox = process.platform !== "linux",
265265
args = [],
266266
extra_args = [],
267267
remote_debugging = "port",

js/test/test.LocalBrowserLauncher.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,20 @@ describe("LocalBrowserLauncher", () => {
6060
);
6161
if (process.platform === "linux") {
6262
expect((chrome.proc as { spawnargs?: string[] }).spawnargs ?? []).toContain("--no-sandbox");
63+
} else {
64+
expect((chrome.proc as { spawnargs?: string[] }).spawnargs ?? []).not.toContain("--no-sandbox");
6365
}
6466
await expect(stat(userDataDir)).resolves.toBeTruthy();
6567
cdp = await CdpSocket.connect(chrome.cdp_url!);
68+
const systemInfo = await cdp.send("SystemInfo.getInfo");
69+
const commandLine = systemInfo.commandLine;
70+
expect(commandLine).toEqual(expect.any(String));
71+
expect(commandLine).toContain("--window-size=900,700");
72+
if (process.platform === "linux") {
73+
expect(commandLine).toContain("--no-sandbox");
74+
} else {
75+
expect(commandLine).not.toContain("--no-sandbox");
76+
}
6677
await expectCdpBrowserSurface(cdp);
6778
} finally {
6879
await cdp?.close();

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
{
22
"name": "modcdp",
3-
"version": "0.0.16",
3+
"version": "0.0.17",
44
"repository": {
55
"type": "git",
66
"url": "git+https://github.com/pirate/modcdp.git"
77
},
88
"files": [
99
"js/scripts/**/*.mjs",
1010
"dist/**/*",
11+
"dist/extension/**/*",
12+
"dist/js/**/*",
1113
"dist/extension.zip",
1214
"extension/src/**/*.ts",
1315
"extension/src/pages/**/*",
@@ -132,7 +134,7 @@
132134
"clean": "rm -rf dist",
133135
"build": "pnpm run build:package && pnpm run build:extension-assets && pnpm run build:python-readme",
134136
"build:clean": "pnpm run clean && pnpm run build",
135-
"build:package": "tsc -p tsconfig.json && rm -rf dist/js/test",
137+
"build:package": "tsc -p tsconfig.json && rm -rf dist/js/test && rm -f dist/.gitignore",
136138
"build:extension-assets": "node js/scripts/build-extension-assets.mjs",
137139
"build:python-readme": "mkdir -p python && if [ README.md -ef python/README.md ]; then :; else cp README.md python/README.md; fi",
138140
"typecheck": "tsc -p tsconfig.json --noEmit",

python/modcdp/launcher/LocalBrowserLauncher.py

Lines changed: 18 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser
7575
default_headless = sys.platform.startswith("linux") and not os.environ.get("DISPLAY")
7676
if merged.get("headless", default_headless):
7777
args.append("--headless=new")
78-
if merged.get("sandbox", False) is False:
78+
default_sandbox = not sys.platform.startswith("linux")
79+
if merged.get("sandbox", default_sandbox) is False:
7980
args.append("--no-sandbox")
8081
args.extend(list(merged.get("args") or []))
8182
args.extend(list(merged.get("extra_args") or []))
@@ -85,10 +86,8 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser
8586
child_read, parent_write = os.pipe()
8687
parent_read = _move_fd_if_needed(parent_read, {3, 4})
8788
parent_write = _move_fd_if_needed(parent_write, {3, 4})
88-
if child_read == 4:
89-
child_read = _move_fd_if_needed(child_read, {4})
90-
if child_write == 3:
91-
child_write = _move_fd_if_needed(child_write, {3})
89+
child_read = _move_fd_to(child_read, 3)
90+
child_write = _move_fd_to(child_write, 4)
9291
process = _spawn_chrome_with_pipe_fds(executable_path, args, child_read, child_write)
9392
os.close(child_read)
9493
os.close(child_write)
@@ -224,61 +223,23 @@ def _move_fd_if_needed(fd: int, reserved: set[int]) -> int:
224223
return moved
225224

226225

227-
class _SpawnedProcess:
228-
def __init__(self, pid: int) -> None:
229-
self.pid = pid
230-
self.returncode: int | None = None
231-
232-
def terminate(self) -> None:
233-
self._signal(signal.SIGTERM)
234-
235-
def kill(self) -> None:
236-
self._signal(signal.SIGKILL)
237-
238-
def wait(self, timeout: float | None = None) -> int:
239-
deadline = None if timeout is None else time.monotonic() + timeout
240-
while True:
241-
try:
242-
waited_pid, status = os.waitpid(self.pid, os.WNOHANG)
243-
except ChildProcessError:
244-
if self.returncode is None:
245-
self.returncode = 0
246-
return self.returncode
247-
if waited_pid == self.pid:
248-
self.returncode = os.waitstatus_to_exitcode(status)
249-
return self.returncode
250-
if deadline is not None and time.monotonic() >= deadline:
251-
assert timeout is not None
252-
raise subprocess.TimeoutExpired([str(self.pid)], timeout)
253-
time.sleep(0.05)
254-
255-
def _signal(self, sig: int) -> None:
256-
if self.returncode is not None:
257-
return
258-
try:
259-
os.kill(self.pid, sig)
260-
except ProcessLookupError:
261-
self.returncode = 0
226+
def _move_fd_to(fd: int, target: int) -> int:
227+
if fd == target:
228+
return fd
229+
os.dup2(fd, target)
230+
os.close(fd)
231+
return target
262232

263233

264234
def _spawn_chrome_with_pipe_fds(executable_path: str, args: list[str], child_read: int, child_write: int) -> _ChromeProcess:
265-
if not hasattr(os, "posix_spawn"):
266-
raise RuntimeError("remote_debugging='pipe' requires os.posix_spawn support in the Python client.")
267-
devnull = os.open(os.devnull, os.O_RDWR)
268-
file_actions: list[tuple[int, int] | tuple[int, int, int]] = [
269-
(os.POSIX_SPAWN_DUP2, devnull, 0),
270-
(os.POSIX_SPAWN_DUP2, devnull, 1),
271-
(os.POSIX_SPAWN_DUP2, devnull, 2),
272-
(os.POSIX_SPAWN_DUP2, child_read, 3),
273-
(os.POSIX_SPAWN_DUP2, child_write, 4),
274-
]
275-
for fd in {devnull, child_read, child_write} - {0, 1, 2, 3, 4}:
276-
file_actions.append((os.POSIX_SPAWN_CLOSE, fd))
277-
try:
278-
pid = os.posix_spawn(executable_path, [executable_path, *args], os.environ, file_actions=file_actions, setsid=True)
279-
return _SpawnedProcess(pid)
280-
finally:
281-
os.close(devnull)
235+
return subprocess.Popen(
236+
[executable_path, *args],
237+
stdin=subprocess.DEVNULL,
238+
stdout=subprocess.DEVNULL,
239+
stderr=subprocess.DEVNULL,
240+
pass_fds=(child_read, child_write),
241+
start_new_session=not sys.platform.startswith("win"),
242+
)
282243

283244

284245
class _ChromeProcess(Protocol):

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "modcdp"
7-
version = "0.0.16"
7+
version = "0.0.17"
88
description = "Python client for ModCDP."
99
readme = "README.md"
1010
requires-python = ">=3.11"

python/tests/test_LocalBrowserLauncher.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import sys
45
import tempfile
56
import unittest
67
from pathlib import Path
@@ -15,7 +16,7 @@ def test_class_helpers_match_ts_surface(self) -> None:
1516
self.assertIsInstance(LocalBrowserLauncher.findChromeBinary(), str)
1617
self.assertIsInstance(LocalBrowserLauncher.freePort(), int)
1718

18-
def test_launches_real_browser_and_speaks_cdp(self) -> None:
19+
def test_launches_real_browser_over_chosen_cdp_port_and_honors_launch_options(self) -> None:
1920
with tempfile.TemporaryDirectory(prefix="modcdp-python-local-profile-") as user_data_dir:
2021
chrome = LocalBrowserLauncher(
2122
{
@@ -36,6 +37,16 @@ def test_launches_real_browser_and_speaks_cdp(self) -> None:
3637
self.assertEqual(version["id"], 1)
3738
self.assertIn("Chrome", version["result"]["product"])
3839
self.assertIsInstance(version["result"]["protocolVersion"], str)
40+
ws.send(json.dumps({"id": 2, "method": "SystemInfo.getInfo", "params": {}}))
41+
system_info = json.loads(ws.recv())
42+
self.assertEqual(system_info["id"], 2)
43+
command_line = system_info["result"]["commandLine"]
44+
self.assertIsInstance(command_line, str)
45+
self.assertIn("--window-size=900,700", command_line)
46+
if sys.platform.startswith("linux"):
47+
self.assertIn("--no-sandbox", command_line)
48+
else:
49+
self.assertNotIn("--no-sandbox", command_line)
3950
finally:
4051
ws.close()
4152
chrome["close"]()

python/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)