camoufox-cli 0.7.1__tar.gz → 0.7.2__tar.gz
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.
Potentially problematic release.
This version of camoufox-cli might be problematic. Click here for more details.
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/PKG-INFO +1 -1
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/package-lock.json +2 -2
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/package.json +1 -1
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/src/browser.ts +18 -3
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/src/cli.ts +14 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/src/server.ts +26 -12
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/tests/browser.test.ts +24 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/tests/server.test.ts +43 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/pyproject.toml +1 -1
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/cli.py +15 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/.claude-plugin/marketplace.json +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/.gitignore +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/LICENSE +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/Makefile +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/README.md +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/.gitignore +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/LICENSE +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/package-lock.json +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/package.json +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/pnpm-lock.yaml +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/scripts/postinstall.js +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/browser.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/cli.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/commands.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/config.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/daemon.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/identity.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/install.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/protocol.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/proxy.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/refs.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/server.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/tests/browser.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/tests/cli.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/tests/commands.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/tests/e2e.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/tests/install.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/tests/protocol.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/tests/proxy.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/tests/refs.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/tests/server.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/tsconfig.json +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/vitest.config.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/pyproject.toml +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/skills/camoufox-cli/SKILL.md +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/skills/camoufox-cli/references/commands.md +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/skills/camoufox-cli/references/snapshot-refs.md +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/__init__.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/__main__.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/browser.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/cli.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/commands.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/config.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/identity.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/install.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/protocol.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/proxy.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/refs.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/src/camoufox_cli/server.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/tests/__init__.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/tests/fixture.html +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/tests/test_cli.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/tests/test_commands.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/tests/test_e2e.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/tests/test_install.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/tests/test_protocol.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/tests/test_proxy.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/tests/test_refs.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/tests/test_server.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude-plugin/marketplace.json +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.gitignore +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/LICENSE +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/Makefile +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/README.md +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/.gitignore +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/LICENSE +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/pnpm-lock.yaml +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/scripts/postinstall.js +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/src/commands.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/src/config.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/src/daemon.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/src/identity.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/src/install.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/src/protocol.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/src/proxy.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/src/refs.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/tests/cli.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/tests/commands.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/tests/e2e.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/tests/install.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/tests/protocol.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/tests/proxy.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/tests/refs.test.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/tsconfig.json +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/js/vitest.config.ts +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/skills/camoufox-cli/SKILL.md +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/skills/camoufox-cli/references/commands.md +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/skills/camoufox-cli/references/snapshot-refs.md +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/__init__.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/__main__.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/browser.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/commands.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/config.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/identity.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/install.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/protocol.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/proxy.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/refs.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/src/camoufox_cli/server.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/tests/__init__.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/tests/fixture.html +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/tests/test_cli.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/tests/test_commands.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/tests/test_e2e.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/tests/test_install.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/tests/test_protocol.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/tests/test_proxy.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/tests/test_refs.py +0 -0
- {camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/tests/test_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: camoufox-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Anti-detect browser CLI & Skills for AI agents, powered by Camoufox
|
|
5
5
|
Project-URL: Homepage, https://github.com/Bin-Huang/camoufox-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/Bin-Huang/camoufox-cli
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "camoufox-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "camoufox-cli",
|
|
9
|
-
"version": "0.7.
|
|
9
|
+
"version": "0.7.2",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
@@ -70,6 +70,8 @@ export class BrowserManager {
|
|
|
70
70
|
private tabs = new Map<string, TabState>();
|
|
71
71
|
private launching: Promise<void> | null = null;
|
|
72
72
|
private recovering: Promise<void> | null = null;
|
|
73
|
+
// Serializes releaseTab calls; see releaseTab for why.
|
|
74
|
+
private releasing: Promise<void> = Promise.resolve();
|
|
73
75
|
private persistent: string | null;
|
|
74
76
|
private proxy: string | null;
|
|
75
77
|
private geoip: boolean;
|
|
@@ -266,10 +268,23 @@ export class BrowserManager {
|
|
|
266
268
|
* which would hijack another agent's tab), and when no live tab remains
|
|
267
269
|
* the browser itself is closed. Idempotent: releasing a tab that has no
|
|
268
270
|
* open page is a no-op, not an error.
|
|
271
|
+
*
|
|
272
|
+
* Releases are SERIALIZED through a promise chain. Without it, N
|
|
273
|
+
* concurrent closes interleave at the awaits: every one deletes its own
|
|
274
|
+
* map entry first, then every survivor scan sees an emptied map, so all
|
|
275
|
+
* N conclude they are the last tab out — the browser gets torn down N
|
|
276
|
+
* times and isRunning flips false for every in-flight close, which made
|
|
277
|
+
* the server destroy sibling connections before their responses flushed
|
|
278
|
+
* (their tabs were released, but the clients never got the receipt).
|
|
269
279
|
*/
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
//
|
|
280
|
+
releaseTab(tab: string): Promise<void> {
|
|
281
|
+
const run = this.releasing.then(() => this.doReleaseTab(tab));
|
|
282
|
+
// Keep the chain usable even if a release fails.
|
|
283
|
+
this.releasing = run.catch(() => {});
|
|
284
|
+
return run;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async doReleaseTab(tab: string): Promise<void> {
|
|
273
288
|
const st = this.tabs.get(tab);
|
|
274
289
|
this.tabs.delete(tab);
|
|
275
290
|
if (st?.page && !st.page.isClosed()) {
|
|
@@ -493,10 +493,24 @@ async function main() {
|
|
|
493
493
|
return;
|
|
494
494
|
} catch (e: any) {
|
|
495
495
|
lastErr = e.message || String(e);
|
|
496
|
+
// close is idempotent: if the daemon is gone (socket removed), there is
|
|
497
|
+
// nothing left to close — that IS success. This is the receipt for a
|
|
498
|
+
// close that raced the last-tab shutdown: the daemon may exit after
|
|
499
|
+
// releasing our tab but before our response could be delivered.
|
|
500
|
+
if (action === "close" && !fs.existsSync(sockPath)) {
|
|
501
|
+
if (flags.json) console.log(JSON.stringify({ id: "r1", success: true, data: { closed: true } }));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
496
504
|
if (attempt < 4) await new Promise((r) => setTimeout(r, 200 * (attempt + 1)));
|
|
497
505
|
}
|
|
498
506
|
}
|
|
499
507
|
|
|
508
|
+
// Same idempotent-close check once more: the daemon may have finished
|
|
509
|
+
// unlinking its socket only while we were burning the retry budget.
|
|
510
|
+
if (action === "close" && !fs.existsSync(sockPath)) {
|
|
511
|
+
if (flags.json) console.log(JSON.stringify({ id: "r1", success: true, data: { closed: true } }));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
500
514
|
process.stderr.write(`Error: Failed to connect to daemon after 5 attempts: ${lastErr}\n`);
|
|
501
515
|
process.exit(1);
|
|
502
516
|
}
|
|
@@ -21,6 +21,9 @@ export class DaemonServer {
|
|
|
21
21
|
// the server side lingering until we destroy it. Track live connections so
|
|
22
22
|
// shutdown can drop them — otherwise server.close() never emits 'close'.
|
|
23
23
|
private connections = new Set<net.Socket>();
|
|
24
|
+
// Connections currently processing a request; closeServer defers their
|
|
25
|
+
// destruction until the response has flushed.
|
|
26
|
+
private busy = new Set<net.Socket>();
|
|
24
27
|
|
|
25
28
|
constructor(opts: { session?: string; headless?: boolean; timeout?: number; persistent?: string | null; proxy?: string | null; geoip?: boolean; locale?: string | null }) {
|
|
26
29
|
this.session = opts.session ?? "default";
|
|
@@ -67,7 +70,7 @@ export class DaemonServer {
|
|
|
67
70
|
|
|
68
71
|
private handleConnection(conn: net.Socket): void {
|
|
69
72
|
this.connections.add(conn);
|
|
70
|
-
conn.on("close", () => this.connections.delete(conn));
|
|
73
|
+
conn.on("close", () => { this.connections.delete(conn); this.busy.delete(conn); });
|
|
71
74
|
|
|
72
75
|
let data = "";
|
|
73
76
|
let handled = false;
|
|
@@ -77,6 +80,10 @@ export class DaemonServer {
|
|
|
77
80
|
const nlIdx = data.indexOf("\n");
|
|
78
81
|
if (nlIdx < 0) return;
|
|
79
82
|
handled = true;
|
|
83
|
+
// Mark mid-request: closeServer must let this connection's response
|
|
84
|
+
// flush instead of destroying it (its client would otherwise never
|
|
85
|
+
// learn that its command actually succeeded).
|
|
86
|
+
this.busy.add(conn);
|
|
80
87
|
|
|
81
88
|
this.lastActivity = Date.now();
|
|
82
89
|
const line = data.slice(0, nlIdx).trim();
|
|
@@ -97,10 +104,7 @@ export class DaemonServer {
|
|
|
97
104
|
// tabs keep the daemon alive, so a non-final close just responds and
|
|
98
105
|
// its connection cleans up on the normal path.
|
|
99
106
|
if (command.action === "close" && !this.manager.isRunning) {
|
|
100
|
-
|
|
101
|
-
// flushed, then drop the rest via closeServer().
|
|
102
|
-
conn.once("finish", () => conn.destroy());
|
|
103
|
-
this.closeServer(conn);
|
|
107
|
+
this.closeServer();
|
|
104
108
|
}
|
|
105
109
|
} catch (e: any) {
|
|
106
110
|
conn.end(Buffer.from(JSON.stringify({ id: "?", success: false, error: String(e) }) + "\n"));
|
|
@@ -169,16 +173,26 @@ export class DaemonServer {
|
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
/**
|
|
172
|
-
* Stop the server and drop
|
|
173
|
-
*
|
|
174
|
-
* them alive and server.close() never completes, so the daemon hangs
|
|
175
|
-
*
|
|
176
|
-
*
|
|
176
|
+
* Stop the server and drop lingering connections so it can actually emit
|
|
177
|
+
* 'close'. Without destroying idle half-open connections, allowHalfOpen
|
|
178
|
+
* keeps them alive and server.close() never completes, so the daemon hangs
|
|
179
|
+
* forever. Connections that are mid-request (concurrent commands — e.g.
|
|
180
|
+
* several agents' closes racing the last-tab shutdown) are NOT cut: their
|
|
181
|
+
* work has already executed, so destroying them would eat the response and
|
|
182
|
+
* make the client report a failure for a command that succeeded. They are
|
|
183
|
+
* destroyed once their response flushes ('finish'), with a timeout backstop
|
|
184
|
+
* so a wedged handler can't keep the daemon alive forever.
|
|
177
185
|
*/
|
|
178
|
-
private closeServer(
|
|
186
|
+
private closeServer(): void {
|
|
179
187
|
this.server?.close();
|
|
180
188
|
for (const c of this.connections) {
|
|
181
|
-
if (c
|
|
189
|
+
if (!this.busy.has(c) || c.writableFinished) {
|
|
190
|
+
c.destroy();
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const backstop = setTimeout(() => c.destroy(), 10_000);
|
|
194
|
+
backstop.unref();
|
|
195
|
+
c.once("finish", () => { clearTimeout(backstop); c.destroy(); });
|
|
182
196
|
}
|
|
183
197
|
}
|
|
184
198
|
|
|
@@ -29,6 +29,30 @@ describe("BrowserManager", () => {
|
|
|
29
29
|
expect(launches).toBe(2);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
it("concurrent releases: only the true last tab out closes the browser", async () => {
|
|
33
|
+
const manager = new BrowserManager();
|
|
34
|
+
let closes = 0;
|
|
35
|
+
vi.spyOn(manager, "close").mockImplementation(async () => { closes++; });
|
|
36
|
+
// Fake pages that take a moment to close, forcing the releases to
|
|
37
|
+
// interleave at the await — the exact shape of the production race.
|
|
38
|
+
const mkTab = () => ({
|
|
39
|
+
page: { isClosed: () => false, close: () => new Promise((r) => setTimeout(r, 20)) },
|
|
40
|
+
});
|
|
41
|
+
(manager as any).tabs.set("a", mkTab());
|
|
42
|
+
(manager as any).tabs.set("b", mkTab());
|
|
43
|
+
(manager as any).tabs.set("c", mkTab());
|
|
44
|
+
|
|
45
|
+
await Promise.all([
|
|
46
|
+
manager.releaseTab("a"),
|
|
47
|
+
manager.releaseTab("b"),
|
|
48
|
+
manager.releaseTab("c"),
|
|
49
|
+
]);
|
|
50
|
+
// Unserialized, every release sees an emptied map and all three "close
|
|
51
|
+
// the browser"; serialized, only the genuine last one does.
|
|
52
|
+
expect(closes).toBe(1);
|
|
53
|
+
expect((manager as any).tabs.size).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
32
56
|
it("getPage rejects when not launched", async () => {
|
|
33
57
|
const manager = new BrowserManager();
|
|
34
58
|
await expect(manager.getPage()).rejects.toThrow("not launched");
|
|
@@ -178,6 +178,49 @@ describe("DaemonServer", () => {
|
|
|
178
178
|
await serverPromise;
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
it("concurrent closes all receive a response (none eaten by shutdown)", async () => {
|
|
182
|
+
// Regression: N agents closing at once. The first close to complete used
|
|
183
|
+
// to see isRunning=false and destroy every sibling connection before its
|
|
184
|
+
// response flushed — the tabs were released but the clients reported
|
|
185
|
+
// failure. All N clients must get a parseable success response.
|
|
186
|
+
const server = new DaemonServer({ session: TEST_SESSION, timeout: 5 });
|
|
187
|
+
const serverPromise = server.start();
|
|
188
|
+
for (let i = 0; i < 50; i++) {
|
|
189
|
+
if (fs.existsSync(SOCK_PATH)) break;
|
|
190
|
+
await new Promise(r => setTimeout(r, 100));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const sendClose = (id: string) => new Promise<string>((resolve, reject) => {
|
|
194
|
+
const client = net.createConnection(SOCK_PATH, () => {
|
|
195
|
+
client.end(JSON.stringify({ id, action: "close", params: {} }) + "\n");
|
|
196
|
+
});
|
|
197
|
+
let data = "";
|
|
198
|
+
client.on("data", chunk => { data += chunk.toString(); });
|
|
199
|
+
client.on("end", () => resolve(data));
|
|
200
|
+
client.on("error", reject);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const responses = await Promise.all(
|
|
204
|
+
["c1", "c2", "c3", "c4", "c5", "c6"].map(sendClose)
|
|
205
|
+
);
|
|
206
|
+
// Every connection whose request began processing must get a full,
|
|
207
|
+
// parseable response (the old code destroyed them mid-response). A
|
|
208
|
+
// sibling that had not even been read yet may be dropped at shutdown —
|
|
209
|
+
// the real CLI covers that via its idempotent-close fallback — but a
|
|
210
|
+
// partial/truncated response is never acceptable.
|
|
211
|
+
let delivered = 0;
|
|
212
|
+
for (const raw of responses) {
|
|
213
|
+
if (raw === "") continue;
|
|
214
|
+
const parsed = JSON.parse(raw); // truncated raw -> test fails here
|
|
215
|
+
expect(parsed.success).toBe(true);
|
|
216
|
+
delivered++;
|
|
217
|
+
}
|
|
218
|
+
expect(delivered).toBeGreaterThan(0);
|
|
219
|
+
|
|
220
|
+
await serverPromise; // daemon still shuts down cleanly afterwards
|
|
221
|
+
expect(fs.existsSync(SOCK_PATH)).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
181
224
|
it("cleans up socket and pid on shutdown", async () => {
|
|
182
225
|
const server = new DaemonServer({
|
|
183
226
|
session: TEST_SESSION,
|
|
@@ -502,9 +502,24 @@ def main():
|
|
|
502
502
|
return
|
|
503
503
|
except Exception as e:
|
|
504
504
|
last_err = str(e)
|
|
505
|
+
# close is idempotent: if the daemon is gone (socket removed),
|
|
506
|
+
# there is nothing left to close — that IS success. This is the
|
|
507
|
+
# receipt for a close that raced the last-tab shutdown: the daemon
|
|
508
|
+
# may exit after releasing our tab but before responding.
|
|
509
|
+
if action == "close" and not os.path.exists(sock_path):
|
|
510
|
+
if flags["json"]:
|
|
511
|
+
print(json.dumps({"id": "r1", "success": True, "data": {"closed": True}}))
|
|
512
|
+
return
|
|
505
513
|
if attempt < 4:
|
|
506
514
|
time.sleep(0.2 * (attempt + 1))
|
|
507
515
|
|
|
516
|
+
# Same idempotent-close check once more: the daemon may have finished
|
|
517
|
+
# unlinking its socket only while we were burning the retry budget.
|
|
518
|
+
if action == "close" and not os.path.exists(sock_path):
|
|
519
|
+
if flags["json"]:
|
|
520
|
+
print(json.dumps({"id": "r1", "success": True, "data": {"closed": True}}))
|
|
521
|
+
return
|
|
522
|
+
|
|
508
523
|
print(f"Error: Failed to connect to daemon after 5 attempts: {last_err}", file=sys.stderr)
|
|
509
524
|
sys.exit(1)
|
|
510
525
|
|
|
File without changes
|
{camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/.gitignore
RENAMED
|
File without changes
|
{camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/LICENSE
RENAMED
|
File without changes
|
{camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/Makefile
RENAMED
|
File without changes
|
{camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/README.md
RENAMED
|
File without changes
|
{camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/.gitignore
RENAMED
|
File without changes
|
{camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{camoufox_cli-0.7.1 → camoufox_cli-0.7.2}/.claude/worktrees/cli-version-command-8852a1/js/src/cli.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|