Skip to content

Commit 139daab

Browse files
DavertMikclaude
andcommitted
feat(Obscura): self-managed server lifecycle with auto port selection
Obscura now manages its own obscura serve process the same way Playwright manages its own browser: leaving endpoint unset resolves a binary (binaryPath, then OBSCURA_PATH, then PATH), spawns it on a free port, and kills it on teardown, so run-workers instances never collide on a port. Setting endpoint explicitly switches to attach-only mode; if no binary can be resolved but something already answers on the conventional :9222, the helper courtesy-attaches instead of failing. CI stops pre-starting obscura serve and exercises self-launch via OBSCURA_PATH instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent c07cfa2 commit 139daab

5 files changed

Lines changed: 309 additions & 113 deletions

File tree

.github/workflows/obscura.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ jobs:
5757
echo "${OBSCURA_SHA256} $HOME/obscura-bin/obscura.tar.gz" | sha256sum -c -
5858
tar xzf ~/obscura-bin/obscura.tar.gz -C ~/obscura-bin
5959
~/obscura-bin/obscura --version
60-
- name: start test server and obscura
60+
- name: start test server
6161
run: |
6262
php -S 127.0.0.1:8000 -t test/data/app &
63-
~/obscura-bin/obscura serve --port 9222 --allow-private-network &
64-
sleep 2
65-
curl -sf http://127.0.0.1:9222/json/version
63+
sleep 1
6664
curl -sf http://127.0.0.1:8000/info > /dev/null
6765
- name: run obscura helper tests
68-
run: npm run test:unit:webbapi:obscura
66+
run: |
67+
export OBSCURA_PATH="$HOME/obscura-bin/obscura"
68+
npm run test:unit:webbapi:obscura

docs/alternative-browsers.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ headless Chromium.
8080
helpers: {
8181
Obscura: {
8282
url: 'http://localhost:3000',
83-
binaryPath: '/usr/local/bin/obscura',
8483
},
8584
}
8685

@@ -101,6 +100,41 @@ Any other CDP endpoint works through the base helper:
101100
},
102101
}
103102

103+
### Obscura's three connection modes
104+
105+
Obscura manages its own `obscura serve` process, the same way Playwright manages its own browser
106+
process — there is nothing to start by hand in the common case:
107+
108+
- **Self-launch (default)** — leave `endpoint` unset. The helper resolves a binary
109+
(`binaryPath` in the config, then `OBSCURA_PATH`, then `obscura` on `PATH`), spawns
110+
`obscura serve` on a free port, and kills it when the run ends. Not setting `port` is
111+
intentional: a free port is picked automatically, which is what makes `run-workers`
112+
collision-free — every worker gets its own instance without any config.
113+
114+
helpers: {
115+
Obscura: {
116+
url: 'http://localhost:3000',
117+
binaryPath: '/usr/local/bin/obscura', // optional override, like Playwright's executablePath
118+
},
119+
}
120+
121+
- **Attach** — set `endpoint` explicitly to connect to an Obscura instance you manage yourself
122+
(already running locally, in a container, or on a remote host). The helper only connects; it
123+
never spawns or kills anything.
124+
125+
helpers: {
126+
Obscura: {
127+
url: 'http://localhost:3000',
128+
endpoint: 'http://127.0.0.1:9222',
129+
},
130+
}
131+
132+
- **Courtesy-attach** — only relevant when `endpoint` is unset and no binary can be resolved
133+
either. If something is already answering on the conventional `http://127.0.0.1:9222`, the
134+
helper attaches to it (and, again, never kills it) instead of failing outright. This exists so
135+
that "start Obscura by hand and just run the tests" keeps working without any config, while
136+
self-launch is still the default for everyone else.
137+
104138
## Capability matrix
105139

106140
| | Playwright | Obscura | Kitesurf |

docs/helpers/Obscura.md

Lines changed: 102 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,45 @@ given binary is in — `CDPBrowser._probeCapabilities` detects `layout`/`screens
1919
runtime, so the same helper works against either.
2020

2121
This helper is a thin `CDPBrowser` subclass: it changes nothing about how locating or acting on
22-
elements works, it only pins the config presets Obscura requires and, optionally, spawns/tears
23-
down the `obscura serve` process around the test run.
22+
elements works, it only pins the config presets Obscura requires and manages the `obscura serve`
23+
process lifecycle, the same way Playwright manages its own browser process.
24+
25+
## Modes
26+
27+
* **ATTACH**`endpoint` is set explicitly in the config. The helper only connects to it; it
28+
never spawns or kills anything, no matter what `binaryPath`/`port` are set to.
29+
* **SELF-LAUNCH**`endpoint` is unset and a binary can be resolved, in order: `binaryPath` in
30+
the config, then the `OBSCURA_PATH` environment variable, then `obscura` on `PATH`. The helper
31+
spawns `obscura serve --port <port> --allow-private-network` (`port` from the config, or a
32+
free port picked automatically), waits for it to answer, connects, and kills it in
33+
`_finishTest`.
34+
* **COURTESY-ATTACH**`endpoint` is unset and no binary can be resolved, but something already
35+
answers `http://127.0.0.1:9222/json/version` (e.g. `obscura serve` started by hand, or by CI
36+
before this process ever ran). The helper attaches to it and never kills it — it isn't the
37+
helper's process to kill. If neither a binary nor a running server on :9222 can be found, the
38+
helper throws a loud, actionable error.
2439

2540
## Install
2641

27-
Download a release binary and put it on your `PATH` (or point `binaryPath` at it directly):
42+
Download a release binary and put it on your `PATH` (or point `binaryPath`/`OBSCURA_PATH` at
43+
it directly) and the helper launches and tears it down for you automatically:
2844

2945
```sh
3046
curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.0/obscura-x86_64-linux.tar.gz | tar xz
3147
```
3248

33-
Then either let this helper manage the process (set `binaryPath`), or start it yourself before
34-
the test run:
35-
36-
```sh
37-
obscura serve --port 9222 --allow-private-network
38-
```
39-
40-
`--allow-private-network` is required to reach apps running on `localhost`/private IPs (e.g. a
41-
dev server on `127.0.0.1:8000`) — Obscura blocks private-network requests by default.
49+
`--allow-private-network` is always passed by this helper (it's required to reach apps running
50+
on `localhost`/private IPs, e.g. a dev server on `127.0.0.1:8000` — Obscura blocks
51+
private-network requests by default).
4252

4353
## Config presets
4454

4555
These are set automatically and only need overriding for unusual setups:
4656

47-
| option | value | why |
48-
| --------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49-
| `endpoint` | `ws://127.0.0.1:<port>/devtools/browser` | Obscura's fixed CDP endpoint |
50-
| `input` | `synthetic` | coordinate-click navigation is unreliable over CDP on Obscura even on rendering builds (no `frameNavigated` event, stale `page.url()`); `click` always takes the `forceClick` path — on Obscura, `click` and `forceClick` are the same thing |
51-
| `xpathPolyfill` | `auto` | probed per binary/page: Obscura's native `document.evaluate` still doesn't support attribute selection or `not()`, so the polyfill is used until that lands |
57+
| option | value | why |
58+
| --------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59+
| `input` | `synthetic` | coordinate-click navigation is unreliable over CDP on Obscura even on rendering builds (no `frameNavigated` event, stale `page.url()`); `click` always takes the `forceClick` path — on Obscura, `click` and `forceClick` are the same thing |
60+
| `xpathPolyfill` | `auto` | probed per binary/page: Obscura's native `document.evaluate` still doesn't support attribute selection or `not()`, so the polyfill is used until that lands |
5261

5362
`capabilities.layout`/`capabilities.screenshot`/`capabilities.xpath` are intentionally left
5463
unset here — `CDPBrowser._probeCapabilities` detects them at runtime from the actual binary
@@ -74,30 +83,45 @@ Set them explicitly in your own config to skip probing or to force a mode.
7483
This helper should be configured in codecept.conf.js. It accepts everything `CDPBrowser`
7584
accepts (see its config table), plus:
7685

77-
Type: [object][2]
86+
Type: [object][6]
7887

7988
### Properties
8089

81-
* `binaryPath` **[string][3]?** path to the `obscura` executable. When set, `_connect` spawns
82-
`obscura serve --port <port> --allow-private-network` before connecting and kills that process
83-
in `_finishTest`. When unset, Obscura is assumed to already be running (e.g. started by hand, or
84-
by CI) and this helper only connects to it.
85-
* `port` **[number][4]?** port `obscura serve` listens on, and the port used to build the
86-
default `endpoint`.
87-
* `serverStartTimeout` **[number][4]?** milliseconds to wait for a spawned `obscura serve`
90+
* `endpoint` **[string][4]?** explicit CDP endpoint. Setting this switches the helper to ATTACH
91+
mode: it only connects, and never spawns or kills a process, no matter what else is configured.
92+
Leave it unset for SELF-MANAGED mode (see below).
93+
* `binaryPath` **[string][4]?** path to the `obscura` executable, used in SELF-MANAGED mode
94+
(`endpoint` unset). Checked before `OBSCURA_PATH` and `PATH`.
95+
* `port` **[number][3]?** port `obscura serve` listens on, in SELF-MANAGED mode. When unset, a
96+
free port is picked automatically, which is what makes `run-workers` collision-free — every
97+
worker gets its own instance on its own port with zero config.
98+
* `serverStartTimeout` **[number][3]?** milliseconds to wait for a spawned `obscura serve`
8899
to answer `/json/version` before `_connect` gives up.
89100

90101

91102

92103
## Example
93104

94105
```js
95-
// inside codecept.conf.js
106+
// inside codecept.conf.js — SELF-LAUNCH mode (recommended): the helper finds/starts/stops
107+
// obscura serve on its own, on a free port. Ideal for run-workers: every worker gets its own
108+
// instance with no config.
96109
{
97110
helpers: {
98111
Obscura: {
99112
url: 'http://localhost',
100-
binaryPath: '/usr/local/bin/obscura',
113+
}
114+
}
115+
}
116+
```
117+
118+
```js
119+
// ATTACH mode — connect to an Obscura instance you manage yourself (remote host, container, etc.)
120+
{
121+
helpers: {
122+
Obscura: {
123+
url: 'http://localhost',
124+
endpoint: 'http://127.0.0.1:9222',
101125
}
102126
}
103127
}
@@ -111,22 +135,57 @@ Type: [object][2]
111135

112136
### _connect
113137

114-
Spawns `obscura serve` when `options.binaryPath` is set and no server was spawned yet, waits
115-
for it to accept connections, then connects as `CDPBrowser._connect` normally would.
138+
In ATTACH mode, connects exactly as `CDPBrowser._connect` would. In SELF-MANAGED mode,
139+
resolves and spawns `obscura serve` (or courtesy-attaches to an already-running one on
140+
:9222) exactly once via `_resolveSelfManaged`, then connects.
116141

117-
A spawn failure (e.g. a bad `binaryPath`) is delivered asynchronously by Node as an `error`
142+
A spawn failure (e.g. a bad binary) is delivered asynchronously by Node as an `error`
118143
event; it is recorded on `this.serverError` and surfaced as a rejection from `_waitForServer`
119144
instead of crashing the process as an uncaught exception.
120145

146+
### _findFreePort
147+
148+
Picks a free TCP port on 127.0.0.1 by briefly listening on port 0 and reading back the OS-assigned
149+
port. Used as the SELF-LAUNCH default when `options.port` isn't explicitly set, so multiple
150+
`run-workers` workers never collide on the same port.
151+
152+
Returns **[Promise][2]<[number][3]>** a free port.
153+
121154
### _finishTest
122155

123156
Closes the CDP connection (via `CDPBrowser._finishTest`), then kills the `obscura serve`
124-
process spawned by `_connect`, if any. Runs in a `finally` so the process is always reaped
125-
even if closing the CDP connection throws. Sends `SIGTERM` first and waits for the process to
126-
exit; a process that ignores `SIGTERM` is escalated to `SIGKILL` after 5s. The promise only
127-
resolves once the child has actually exited (confirmed via the `exit` event, not merely once
128-
`SIGKILL` was sent — the kernel needs a moment to reap it), with a final safety-net timeout so
129-
a stuck child can never keep the event loop alive even if that confirmation is somehow lost.
157+
process spawned by `_connect`, if any (never runs in ATTACH or COURTESY-ATTACH mode, since
158+
`this.serverProcess` is only ever set in SELF-LAUNCH mode). Runs in a `finally` so the process
159+
is always reaped even if closing the CDP connection throws. Sends `SIGTERM` first and waits for
160+
the process to exit; a process that ignores `SIGTERM` is escalated to `SIGKILL` after 5s. The
161+
promise only resolves once the child has actually exited (confirmed via the `exit` event, not
162+
merely once `SIGKILL` was sent — the kernel needs a moment to reap it), with a final safety-net
163+
timeout so a stuck child can never keep the event loop alive even if that confirmation is
164+
somehow lost.
165+
166+
### _probeUp
167+
168+
Probes a `/json/version`-style URL with a short timeout, used for the COURTESY-ATTACH check.
169+
170+
#### Parameters
171+
172+
* `url` **[string][4]**&#x20;
173+
174+
Returns **[Promise][2]<[boolean][5]>** true if the URL answered.
175+
176+
### _resolveBinary
177+
178+
Resolves the `obscura` binary to spawn, in priority order: `options.binaryPath`, then the
179+
`OBSCURA_PATH` environment variable, then `obscura` on `PATH` (via `which`).
180+
181+
Returns **([string][4] | null)** an absolute or relative path to the binary, or null if none resolved.
182+
183+
### _resolveSelfManaged
184+
185+
Resolves how to reach Obscura when no explicit `endpoint` was configured, trying, in order:
186+
spawn a binary (`binaryPath` config, then `OBSCURA_PATH` env, then `obscura` on `PATH`),
187+
courtesy-attach to `http://127.0.0.1:9222` if something already answers there, or throw a
188+
loud, actionable error. Sets `this.options.endpoint` as a side effect.
130189

131190
### _waitForServer
132191

@@ -135,8 +194,12 @@ is set by the spawned process' `error` event, or `options.serverStartTimeout` el
135194

136195
[1]: https://github.com/h4ckf0r0day/obscura
137196

138-
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
197+
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise
198+
199+
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
200+
201+
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
139202

140-
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
203+
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
141204

142-
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
205+
[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

0 commit comments

Comments
 (0)