clawdex-mobile 5.0.5-internal.9 → 5.0.5
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/README.md +2 -1
- package/bin/clawdex.js +1 -1
- package/docs/setup-and-operations.md +15 -3
- package/package.json +1 -1
- package/scripts/setup-wizard.sh +28 -9
- package/scripts/start-bridge-secure.js +231 -24
- package/scripts/stop-services.sh +70 -1
- package/services/rust-bridge/Cargo.lock +2 -1
- package/services/rust-bridge/Cargo.toml +2 -1
- package/services/rust-bridge/src/main.rs +139 -4
- package/vendor/bridge-binaries/darwin-arm64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/darwin-x64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/linux-arm64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/linux-armv7l/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/linux-x64/codex-rust-bridge +0 -0
- package/vendor/bridge-binaries/win32-x64/codex-rust-bridge.exe +0 -0
package/README.md
CHANGED
|
@@ -38,7 +38,8 @@ npm install -g clawdex-mobile@latest
|
|
|
38
38
|
clawdex init
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Then open the mobile app and
|
|
41
|
+
Then open the mobile app and connect using the printed bridge URL/token.
|
|
42
|
+
`clawdex init` now writes config, starts the bridge in the background, and returns you to the shell. Bridge logs go to `.bridge.log`.
|
|
42
43
|
|
|
43
44
|
The npm package is bridge-only. It does not install Expo or the mobile source tree. On supported macOS, Linux, and Windows hosts it uses bundled bridge binaries, so normal startup does not compile Rust.
|
|
44
45
|
The current interactive setup helpers are still macOS/Linux-oriented.
|
package/bin/clawdex.js
CHANGED
|
@@ -12,7 +12,7 @@ function printUsage() {
|
|
|
12
12
|
Commands:
|
|
13
13
|
init [--no-start] [--engine codex|opencode]
|
|
14
14
|
Run interactive bridge onboarding and secure setup.
|
|
15
|
-
By default, this also starts the secure bridge in the
|
|
15
|
+
By default, this also starts the secure bridge in the background.
|
|
16
16
|
Use --no-start to configure only.
|
|
17
17
|
Use --engine to set the preferred backend written to .env.secure.
|
|
18
18
|
|
|
@@ -27,9 +27,9 @@ If you want only one harness, use `--engine codex` or `--engine opencode`.
|
|
|
27
27
|
After `clawdex init`, expected sequence:
|
|
28
28
|
|
|
29
29
|
1. Secure config is written or reused
|
|
30
|
-
2. The bridge starts in the
|
|
31
|
-
3.
|
|
32
|
-
4. Bridge logs
|
|
30
|
+
2. The bridge starts in the background
|
|
31
|
+
3. The wizard prints the bridge URL/token for manual mobile pairing
|
|
32
|
+
4. Bridge logs are written to `.bridge.log`
|
|
33
33
|
|
|
34
34
|
Published npm releases bundle prebuilt bridge binaries for `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `linux-armv7l`, and `win32-x64`. On those hosts, normal bridge startup does not require a Rust compile.
|
|
35
35
|
|
|
@@ -160,6 +160,18 @@ npm run teardown -- --yes
|
|
|
160
160
|
| `EXPO_PUBLIC_ALLOW_INSECURE_REMOTE_BRIDGE` | suppress insecure-HTTP warning |
|
|
161
161
|
| `EXPO_PUBLIC_PRIVACY_POLICY_URL` | in-app Privacy link |
|
|
162
162
|
| `EXPO_PUBLIC_TERMS_OF_SERVICE_URL` | in-app Terms link |
|
|
163
|
+
| `EXPO_PUBLIC_REVENUECAT_IOS_API_KEY` | RevenueCat public SDK key for iOS tip purchases |
|
|
164
|
+
| `EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY` | RevenueCat public SDK key for Android tip purchases |
|
|
165
|
+
| `EXPO_PUBLIC_REVENUECAT_TEST_STORE_API_KEY` | RevenueCat Test Store public SDK key for Expo Go / Store Client tip testing |
|
|
166
|
+
| `EXPO_PUBLIC_REVENUECAT_TIPS_OFFERING_ID` | optional RevenueCat offering identifier for the tip jar (`current` if omitted) |
|
|
167
|
+
|
|
168
|
+
If you enable the optional tip jar:
|
|
169
|
+
|
|
170
|
+
- Configure 4–5 non-subscription products in RevenueCat and attach them to a dedicated Offering
|
|
171
|
+
- Use consumables for repeatable “tip” tiers
|
|
172
|
+
- Enable In-App Purchase for the app’s Apple bundle identifier in App Store Connect / Apple Developer
|
|
173
|
+
- Use the RevenueCat Test Store SDK key in Expo Go; use the real iOS SDK key only in native builds/TestFlight/App Store builds
|
|
174
|
+
- Rebuild the native app after adding `react-native-purchases`
|
|
163
175
|
|
|
164
176
|
## Production Readiness Checklist
|
|
165
177
|
|
package/package.json
CHANGED
package/scripts/setup-wizard.sh
CHANGED
|
@@ -233,6 +233,11 @@ load_existing_engine_selection() {
|
|
|
233
233
|
local raw=""
|
|
234
234
|
local engine=""
|
|
235
235
|
|
|
236
|
+
if [[ "$ENGINE_SELECTION_PRESET" == "true" ]]; then
|
|
237
|
+
sync_active_engine_from_selection
|
|
238
|
+
return 0
|
|
239
|
+
fi
|
|
240
|
+
|
|
236
241
|
raw="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_ENABLED_ENGINES")"
|
|
237
242
|
if [[ -n "$raw" ]] && parse_existing_engine_list_csv "$raw"; then
|
|
238
243
|
engine="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_ACTIVE_ENGINE")"
|
|
@@ -951,6 +956,8 @@ print_existing_setup_summary() {
|
|
|
951
956
|
local network_mode=""
|
|
952
957
|
local harnesses=""
|
|
953
958
|
local source_path=""
|
|
959
|
+
local saved_active_engine="$ACTIVE_ENGINE"
|
|
960
|
+
local -a saved_selected_engines=("${SELECTED_ENGINES[@]}")
|
|
954
961
|
|
|
955
962
|
if [[ ! -f "$SECURE_ENV_FILE" ]]; then
|
|
956
963
|
return 1
|
|
@@ -991,6 +998,9 @@ print_existing_setup_summary() {
|
|
|
991
998
|
echo "bridge.token: present"
|
|
992
999
|
fi
|
|
993
1000
|
echo "source: $source_path"
|
|
1001
|
+
|
|
1002
|
+
SELECTED_ENGINES=("${saved_selected_engines[@]}")
|
|
1003
|
+
ACTIVE_ENGINE="$saved_active_engine"
|
|
994
1004
|
}
|
|
995
1005
|
|
|
996
1006
|
require_security_ack() {
|
|
@@ -1556,13 +1566,12 @@ confirm_phone_network_ready() {
|
|
|
1556
1566
|
esac
|
|
1557
1567
|
}
|
|
1558
1568
|
|
|
1559
|
-
|
|
1560
|
-
rail_echo "Starting bridge in
|
|
1561
|
-
rail_echo "Press Ctrl+C to stop the bridge."
|
|
1569
|
+
start_bridge_background() {
|
|
1570
|
+
rail_echo "Starting bridge in background."
|
|
1562
1571
|
echo ""
|
|
1563
1572
|
(
|
|
1564
1573
|
cd "$ROOT_DIR"
|
|
1565
|
-
|
|
1574
|
+
node "$SCRIPT_DIR/start-bridge-secure.js" --background
|
|
1566
1575
|
)
|
|
1567
1576
|
}
|
|
1568
1577
|
|
|
@@ -1691,6 +1700,7 @@ fi
|
|
|
1691
1700
|
|
|
1692
1701
|
BRIDGE_HOST="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_HOST")"
|
|
1693
1702
|
BRIDGE_PORT="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_PORT")"
|
|
1703
|
+
BRIDGE_TOKEN="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_AUTH_TOKEN")"
|
|
1694
1704
|
NETWORK_MODE="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_NETWORK_MODE")"
|
|
1695
1705
|
if [[ -z "$NETWORK_MODE" ]]; then
|
|
1696
1706
|
NETWORK_MODE="$(infer_network_mode_from_host "$BRIDGE_HOST")"
|
|
@@ -1710,16 +1720,25 @@ fi
|
|
|
1710
1720
|
section "Hatch"
|
|
1711
1721
|
if [[ "$AUTO_START" == "true" ]]; then
|
|
1712
1722
|
rail_echo "Auto-start enabled."
|
|
1713
|
-
rail_echo "Launching bridge in
|
|
1714
|
-
|
|
1715
|
-
exit $?
|
|
1723
|
+
rail_echo "Launching bridge in background..."
|
|
1724
|
+
start_bridge_background || exit $?
|
|
1716
1725
|
else
|
|
1717
1726
|
rail_echo "Auto-start disabled by --no-start."
|
|
1718
1727
|
rail_echo "Skipping bridge launch."
|
|
1719
1728
|
fi
|
|
1720
1729
|
|
|
1721
1730
|
section "Next steps"
|
|
1722
|
-
|
|
1723
|
-
rail_echo "
|
|
1731
|
+
if [[ "$AUTO_START" == "true" ]]; then
|
|
1732
|
+
rail_echo "1) Open the mobile app and use onboarding to connect."
|
|
1733
|
+
rail_echo "Bridge URL: http://$BRIDGE_HOST:$BRIDGE_PORT"
|
|
1734
|
+
if [[ -n "$BRIDGE_TOKEN" ]]; then
|
|
1735
|
+
rail_echo "Bridge token: $BRIDGE_TOKEN"
|
|
1736
|
+
fi
|
|
1737
|
+
rail_echo "Bridge logs: $ROOT_DIR/.bridge.log"
|
|
1738
|
+
rail_echo "2) Stop the bridge later with: clawdex stop"
|
|
1739
|
+
else
|
|
1740
|
+
rail_echo "1) cd $ROOT_DIR && npm run secure:bridge"
|
|
1741
|
+
rail_echo "2) Open the mobile app and use onboarding to connect (URL + token QR)."
|
|
1742
|
+
fi
|
|
1724
1743
|
rail_blank
|
|
1725
1744
|
rail_echo "${DIM}You can rerun this anytime: npm run setup:wizard${RESET}"
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
const { spawn, spawnSync } = require("node:child_process");
|
|
5
5
|
const fs = require("node:fs");
|
|
6
|
+
const http = require("node:http");
|
|
7
|
+
const https = require("node:https");
|
|
6
8
|
const os = require("node:os");
|
|
7
9
|
const path = require("node:path");
|
|
8
10
|
|
|
@@ -13,6 +15,9 @@ const {
|
|
|
13
15
|
resolveRuntimeTarget,
|
|
14
16
|
} = require("./bridge-binary");
|
|
15
17
|
|
|
18
|
+
const DEFAULT_HEALTH_TIMEOUT_MS = 15000;
|
|
19
|
+
const DEV_HEALTH_TIMEOUT_MS = 60000;
|
|
20
|
+
|
|
16
21
|
function resolveRootDir() {
|
|
17
22
|
let rootDir = process.env.INIT_CWD ? path.resolve(process.env.INIT_CWD) : path.resolve(__dirname, "..");
|
|
18
23
|
if (!fs.existsSync(path.join(rootDir, "package.json"))) {
|
|
@@ -50,6 +55,103 @@ function readEnvFile(filePath) {
|
|
|
50
55
|
return nextEnv;
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
function sleep(ms) {
|
|
59
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatHostForUrl(host) {
|
|
63
|
+
if (host.includes(":") && !host.startsWith("[")) {
|
|
64
|
+
return `[${host}]`;
|
|
65
|
+
}
|
|
66
|
+
return host;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function bridgePidFile(rootDir) {
|
|
70
|
+
return path.join(rootDir, ".bridge.pid");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function bridgeLogFile(rootDir) {
|
|
74
|
+
return path.join(rootDir, ".bridge.log");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readPidFile(rootDir) {
|
|
78
|
+
try {
|
|
79
|
+
const raw = fs.readFileSync(bridgePidFile(rootDir), "utf8").trim();
|
|
80
|
+
const pid = Number.parseInt(raw, 10);
|
|
81
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function writePidFile(rootDir, pid) {
|
|
88
|
+
fs.writeFileSync(bridgePidFile(rootDir), `${pid}\n`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function removePidFile(rootDir) {
|
|
92
|
+
try {
|
|
93
|
+
fs.unlinkSync(bridgePidFile(rootDir));
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isProcessAlive(pid) {
|
|
98
|
+
if (!pid) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
process.kill(pid, 0);
|
|
103
|
+
return true;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function waitForHealth(env, pid, timeoutMs) {
|
|
110
|
+
const host = env.BRIDGE_HOST || "127.0.0.1";
|
|
111
|
+
const port = env.BRIDGE_PORT || "8787";
|
|
112
|
+
const url = new URL(`http://${formatHostForUrl(host)}:${port}/health`);
|
|
113
|
+
const startedAt = Date.now();
|
|
114
|
+
|
|
115
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
116
|
+
const ok = await probeHealth(url);
|
|
117
|
+
|
|
118
|
+
if (ok) {
|
|
119
|
+
if (!isProcessAlive(pid)) {
|
|
120
|
+
throw new Error("bridge health endpoint responded, but the started process already exited");
|
|
121
|
+
}
|
|
122
|
+
return { host, port };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!isProcessAlive(pid)) {
|
|
126
|
+
throw new Error("bridge process exited before becoming healthy");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await sleep(500);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw new Error("bridge health check did not recover in time");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function probeHealth(url) {
|
|
136
|
+
const client = url.protocol === "https:" ? https : http;
|
|
137
|
+
return await new Promise((resolve) => {
|
|
138
|
+
const req = client.request(
|
|
139
|
+
url,
|
|
140
|
+
{ method: "GET", timeout: 3000 },
|
|
141
|
+
(response) => {
|
|
142
|
+
resolve(response.statusCode === 200);
|
|
143
|
+
response.resume();
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
req.on("error", () => resolve(false));
|
|
147
|
+
req.on("timeout", () => {
|
|
148
|
+
req.destroy();
|
|
149
|
+
resolve(false);
|
|
150
|
+
});
|
|
151
|
+
req.end();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
53
155
|
function commandExists(command) {
|
|
54
156
|
const checker = process.platform === "win32" ? "where" : "which";
|
|
55
157
|
const result = spawnSync(checker, [command], { stdio: "ignore" });
|
|
@@ -144,6 +246,69 @@ function spawnAndRelay(command, args, options) {
|
|
|
144
246
|
});
|
|
145
247
|
}
|
|
146
248
|
|
|
249
|
+
async function spawnDetachedAndWait(command, args, options) {
|
|
250
|
+
const { cwd, env, rootDir, healthTimeoutMs } = options;
|
|
251
|
+
const logPath = bridgeLogFile(rootDir);
|
|
252
|
+
const host = env.BRIDGE_HOST || "127.0.0.1";
|
|
253
|
+
const port = env.BRIDGE_PORT || "8787";
|
|
254
|
+
const healthUrl = new URL(`http://${formatHostForUrl(host)}:${port}/health`);
|
|
255
|
+
const existingPid = readPidFile(rootDir);
|
|
256
|
+
|
|
257
|
+
if (existingPid && isProcessAlive(existingPid)) {
|
|
258
|
+
if (await probeHealth(healthUrl)) {
|
|
259
|
+
console.log(`Bridge already running (pid ${existingPid}).`);
|
|
260
|
+
console.log(`Logs: ${logPath}`);
|
|
261
|
+
console.log(`Bridge is healthy at http://${formatHostForUrl(host)}:${port}`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
} else if (existingPid) {
|
|
265
|
+
removePidFile(rootDir);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (await probeHealth(healthUrl)) {
|
|
269
|
+
console.error(
|
|
270
|
+
`error: another bridge is already responding at http://${formatHostForUrl(host)}:${port}. Stop it first with 'clawdex stop'.`
|
|
271
|
+
);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const output = fs.openSync(logPath, "a");
|
|
276
|
+
const error = fs.openSync(logPath, "a");
|
|
277
|
+
|
|
278
|
+
const child = spawn(command, args, {
|
|
279
|
+
cwd,
|
|
280
|
+
env,
|
|
281
|
+
detached: true,
|
|
282
|
+
stdio: ["ignore", output, error],
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
child.on("error", (spawnError) => {
|
|
286
|
+
console.error(`error: failed to start ${command}: ${spawnError.message}`);
|
|
287
|
+
removePidFile(rootDir);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (!child.pid) {
|
|
292
|
+
console.error(`error: failed to determine pid for ${command}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
writePidFile(rootDir, child.pid);
|
|
297
|
+
child.unref();
|
|
298
|
+
|
|
299
|
+
console.log(`Bridge starting in background (pid ${child.pid}).`);
|
|
300
|
+
console.log(`Logs: ${logPath}`);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const endpoint = await waitForHealth(env, child.pid, healthTimeoutMs);
|
|
304
|
+
console.log(`Bridge is healthy at http://${formatHostForUrl(endpoint.host)}:${endpoint.port}`);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
removePidFile(rootDir);
|
|
307
|
+
console.error(`error: ${error.message}. Check logs: ${logPath}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
147
312
|
function buildBridgeFromSource(rootDir, env) {
|
|
148
313
|
const cargoCmd = "cargo";
|
|
149
314
|
const args = ["build", "--release", "--locked"];
|
|
@@ -163,30 +328,20 @@ function buildBridgeFromSource(rootDir, env) {
|
|
|
163
328
|
}
|
|
164
329
|
}
|
|
165
330
|
|
|
166
|
-
function
|
|
167
|
-
const rootDir = resolveRootDir();
|
|
168
|
-
const secureEnvFile = path.join(rootDir, ".env.secure");
|
|
169
|
-
if (!fs.existsSync(secureEnvFile)) {
|
|
170
|
-
console.error(`error: ${secureEnvFile} not found. Run: npm run secure:setup`);
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const fileEnv = readEnvFile(secureEnvFile);
|
|
175
|
-
const env = { ...fileEnv, ...process.env };
|
|
176
|
-
const devMode = process.argv.includes("--dev") || env.BRIDGE_RUN_MODE === "dev";
|
|
177
|
-
const forceSourceBuild = env.CLAWDEX_BRIDGE_FORCE_SOURCE_BUILD === "true";
|
|
178
|
-
|
|
331
|
+
function resolveLaunch(rootDir, env, { devMode, forceSourceBuild }) {
|
|
179
332
|
if (devMode) {
|
|
180
333
|
if (!commandExists("cargo")) {
|
|
181
334
|
console.error("error: missing Rust/Cargo toolchain for dev bridge mode.");
|
|
182
335
|
process.exit(1);
|
|
183
336
|
}
|
|
184
337
|
|
|
185
|
-
|
|
338
|
+
return {
|
|
339
|
+
command: "cargo",
|
|
340
|
+
args: ["run"],
|
|
186
341
|
cwd: path.join(rootDir, "services", "rust-bridge"),
|
|
187
342
|
env,
|
|
188
|
-
|
|
189
|
-
|
|
343
|
+
healthTimeoutMs: DEV_HEALTH_TIMEOUT_MS,
|
|
344
|
+
};
|
|
190
345
|
}
|
|
191
346
|
|
|
192
347
|
const overrideBinary = env.CLAWDEX_BRIDGE_BINARY ? path.resolve(env.CLAWDEX_BRIDGE_BINARY) : "";
|
|
@@ -196,22 +351,37 @@ function start() {
|
|
|
196
351
|
process.exit(1);
|
|
197
352
|
}
|
|
198
353
|
ensureExecutable(overrideBinary);
|
|
199
|
-
|
|
200
|
-
|
|
354
|
+
return {
|
|
355
|
+
command: overrideBinary,
|
|
356
|
+
args: [],
|
|
357
|
+
cwd: rootDir,
|
|
358
|
+
env,
|
|
359
|
+
healthTimeoutMs: DEFAULT_HEALTH_TIMEOUT_MS,
|
|
360
|
+
};
|
|
201
361
|
}
|
|
202
362
|
|
|
203
363
|
const packagedBinary = packagedBinaryPath(rootDir, resolveRuntimeTarget());
|
|
204
364
|
if (!forceSourceBuild && packagedBinary && fs.existsSync(packagedBinary)) {
|
|
205
365
|
ensureExecutable(packagedBinary);
|
|
206
|
-
|
|
207
|
-
|
|
366
|
+
return {
|
|
367
|
+
command: packagedBinary,
|
|
368
|
+
args: [],
|
|
369
|
+
cwd: rootDir,
|
|
370
|
+
env,
|
|
371
|
+
healthTimeoutMs: DEFAULT_HEALTH_TIMEOUT_MS,
|
|
372
|
+
};
|
|
208
373
|
}
|
|
209
374
|
|
|
210
375
|
const builtBinary = builtBinaryPath(rootDir, os.platform());
|
|
211
376
|
if (isBuiltBinaryFresh(rootDir, builtBinary)) {
|
|
212
377
|
ensureExecutable(builtBinary);
|
|
213
|
-
|
|
214
|
-
|
|
378
|
+
return {
|
|
379
|
+
command: builtBinary,
|
|
380
|
+
args: [],
|
|
381
|
+
cwd: rootDir,
|
|
382
|
+
env,
|
|
383
|
+
healthTimeoutMs: DEFAULT_HEALTH_TIMEOUT_MS,
|
|
384
|
+
};
|
|
215
385
|
}
|
|
216
386
|
|
|
217
387
|
if (!commandExists("cargo")) {
|
|
@@ -234,7 +404,44 @@ function start() {
|
|
|
234
404
|
}
|
|
235
405
|
|
|
236
406
|
ensureExecutable(builtBinary);
|
|
237
|
-
|
|
407
|
+
return {
|
|
408
|
+
command: builtBinary,
|
|
409
|
+
args: [],
|
|
410
|
+
cwd: rootDir,
|
|
411
|
+
env,
|
|
412
|
+
healthTimeoutMs: DEFAULT_HEALTH_TIMEOUT_MS,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function start() {
|
|
417
|
+
const rootDir = resolveRootDir();
|
|
418
|
+
const secureEnvFile = path.join(rootDir, ".env.secure");
|
|
419
|
+
if (!fs.existsSync(secureEnvFile)) {
|
|
420
|
+
console.error(`error: ${secureEnvFile} not found. Run: npm run secure:setup`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const fileEnv = readEnvFile(secureEnvFile);
|
|
425
|
+
const env = { ...fileEnv, ...process.env };
|
|
426
|
+
const devMode = process.argv.includes("--dev") || env.BRIDGE_RUN_MODE === "dev";
|
|
427
|
+
const backgroundMode = process.argv.includes("--background");
|
|
428
|
+
const forceSourceBuild = env.CLAWDEX_BRIDGE_FORCE_SOURCE_BUILD === "true";
|
|
429
|
+
const launch = resolveLaunch(rootDir, env, { devMode, forceSourceBuild });
|
|
430
|
+
|
|
431
|
+
if (backgroundMode) {
|
|
432
|
+
await spawnDetachedAndWait(launch.command, launch.args, {
|
|
433
|
+
cwd: launch.cwd,
|
|
434
|
+
env: launch.env,
|
|
435
|
+
rootDir,
|
|
436
|
+
healthTimeoutMs: launch.healthTimeoutMs,
|
|
437
|
+
});
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
spawnAndRelay(launch.command, launch.args, { cwd: launch.cwd, env: launch.env });
|
|
238
442
|
}
|
|
239
443
|
|
|
240
|
-
start()
|
|
444
|
+
start().catch((error) => {
|
|
445
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
446
|
+
process.exit(1);
|
|
447
|
+
});
|
package/scripts/stop-services.sh
CHANGED
|
@@ -9,10 +9,68 @@ fi
|
|
|
9
9
|
|
|
10
10
|
BRIDGE_PID_FILE="$ROOT_DIR/.bridge.pid"
|
|
11
11
|
EXPO_PID_FILE="$ROOT_DIR/.expo.pid"
|
|
12
|
+
SECURE_ENV_FILE="$ROOT_DIR/.env.secure"
|
|
12
13
|
|
|
13
14
|
list_matching_pids() {
|
|
14
15
|
local pattern="$1"
|
|
15
|
-
|
|
16
|
+
ps -ax -o pid= -o command= 2>/dev/null | awk -v pattern="$pattern" '
|
|
17
|
+
$0 ~ pattern { print $1 }
|
|
18
|
+
' || true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
stop_pid_file_process() {
|
|
22
|
+
local label="$1"
|
|
23
|
+
local pid_file="$2"
|
|
24
|
+
local pid=""
|
|
25
|
+
|
|
26
|
+
if [[ ! -f "$pid_file" ]]; then
|
|
27
|
+
return 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
pid="$(tr -dc '0-9' <"$pid_file")"
|
|
31
|
+
if [[ -z "$pid" ]]; then
|
|
32
|
+
rm -f "$pid_file"
|
|
33
|
+
return 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
37
|
+
rm -f "$pid_file"
|
|
38
|
+
return 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo "Stopping $label process from pid file: $pid"
|
|
42
|
+
kill -INT "$pid" 2>/dev/null || true
|
|
43
|
+
sleep 1
|
|
44
|
+
|
|
45
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
46
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
47
|
+
sleep 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
51
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
52
|
+
echo "Force stopped $label process: $pid"
|
|
53
|
+
else
|
|
54
|
+
echo "$label stopped."
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
rm -f "$pid_file"
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
extract_env_value() {
|
|
62
|
+
local file="$1"
|
|
63
|
+
local key="$2"
|
|
64
|
+
[[ -f "$file" ]] || return 1
|
|
65
|
+
|
|
66
|
+
awk -F= -v key="$key" '
|
|
67
|
+
$1 == key {
|
|
68
|
+
sub(/^[[:space:]]+/, "", $2)
|
|
69
|
+
sub(/[[:space:]]+$/, "", $2)
|
|
70
|
+
print $2
|
|
71
|
+
exit
|
|
72
|
+
}
|
|
73
|
+
' "$file"
|
|
16
74
|
}
|
|
17
75
|
|
|
18
76
|
stop_process_group() {
|
|
@@ -54,8 +112,19 @@ stop_process_group() {
|
|
|
54
112
|
echo "Stopping Clawdex services for project: $ROOT_DIR"
|
|
55
113
|
|
|
56
114
|
stop_process_group "Expo" "$ROOT_DIR/.*/expo start|$ROOT_DIR/node_modules/.bin/expo start"
|
|
115
|
+
stop_pid_file_process "Rust bridge" "$BRIDGE_PID_FILE" || true
|
|
57
116
|
stop_process_group "Rust bridge" "$ROOT_DIR/services/rust-bridge|codex-rust-bridge|@codex/rust-bridge"
|
|
58
117
|
stop_process_group "Legacy TS bridge" "$ROOT_DIR/services/mac-bridge|@codex/mac-bridge"
|
|
59
118
|
|
|
119
|
+
if [[ -f "$SECURE_ENV_FILE" ]]; then
|
|
120
|
+
BRIDGE_ENABLED_ENGINES="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_ENABLED_ENGINES" || true)"
|
|
121
|
+
BRIDGE_ACTIVE_ENGINE="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_ACTIVE_ENGINE" || true)"
|
|
122
|
+
BRIDGE_OPENCODE_PORT="$(extract_env_value "$SECURE_ENV_FILE" "BRIDGE_OPENCODE_PORT" || true)"
|
|
123
|
+
BRIDGE_OPENCODE_PORT="${BRIDGE_OPENCODE_PORT:-4090}"
|
|
124
|
+
if [[ ",$BRIDGE_ENABLED_ENGINES,$BRIDGE_ACTIVE_ENGINE," == *",opencode,"* ]]; then
|
|
125
|
+
stop_process_group "OpenCode server" "opencode serve --hostname .* --port $BRIDGE_OPENCODE_PORT|\\.opencode serve --hostname .* --port $BRIDGE_OPENCODE_PORT"
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
|
|
60
129
|
rm -f "$BRIDGE_PID_FILE" "$EXPO_PID_FILE"
|
|
61
130
|
echo "Done."
|
|
@@ -149,12 +149,13 @@ dependencies = [
|
|
|
149
149
|
|
|
150
150
|
[[package]]
|
|
151
151
|
name = "codex-rust-bridge"
|
|
152
|
-
version = "5.0.5
|
|
152
|
+
version = "5.0.5"
|
|
153
153
|
dependencies = [
|
|
154
154
|
"axum",
|
|
155
155
|
"base64",
|
|
156
156
|
"chrono",
|
|
157
157
|
"futures-util",
|
|
158
|
+
"libc",
|
|
158
159
|
"qr2term",
|
|
159
160
|
"reqwest",
|
|
160
161
|
"serde",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "codex-rust-bridge"
|
|
3
|
-
version = "5.0.5
|
|
3
|
+
version = "5.0.5"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
|
|
6
6
|
[dependencies]
|
|
@@ -8,6 +8,7 @@ axum = { version = "0.8", features = ["ws", "http1", "tokio"] }
|
|
|
8
8
|
base64 = "0.22"
|
|
9
9
|
chrono = { version = "0.4", features = ["clock", "serde"] }
|
|
10
10
|
futures-util = "0.3"
|
|
11
|
+
libc = "0.2"
|
|
11
12
|
serde = { version = "1", features = ["derive"] }
|
|
12
13
|
serde_json = "1"
|
|
13
14
|
shlex = "1"
|
|
@@ -338,6 +338,15 @@ impl RuntimeBackend {
|
|
|
338
338
|
}))
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
async fn shutdown(&self) {
|
|
342
|
+
if let Some(codex) = &self.codex {
|
|
343
|
+
codex.request_shutdown().await;
|
|
344
|
+
}
|
|
345
|
+
if let Some(opencode) = &self.opencode {
|
|
346
|
+
opencode.request_shutdown().await;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
341
350
|
fn engine(&self) -> BridgeRuntimeEngine {
|
|
342
351
|
self.preferred_engine
|
|
343
352
|
}
|
|
@@ -600,6 +609,95 @@ impl RuntimeBackend {
|
|
|
600
609
|
}
|
|
601
610
|
}
|
|
602
611
|
|
|
612
|
+
fn configure_managed_child_command(command: &mut Command) {
|
|
613
|
+
command.kill_on_drop(true);
|
|
614
|
+
#[cfg(unix)]
|
|
615
|
+
command.process_group(0);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async fn terminate_managed_child(pid: u32, label: &str) {
|
|
619
|
+
#[cfg(unix)]
|
|
620
|
+
{
|
|
621
|
+
terminate_process_group_unix(pid, label).await;
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#[cfg(windows)]
|
|
626
|
+
{
|
|
627
|
+
terminate_process_tree_windows(pid, label).await;
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
#[allow(unreachable_code)]
|
|
632
|
+
let _ = (pid, label);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
#[cfg(unix)]
|
|
636
|
+
async fn wait_for_shutdown_signal() -> &'static str {
|
|
637
|
+
let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
|
|
638
|
+
.expect("failed to install SIGINT handler");
|
|
639
|
+
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
|
640
|
+
.expect("failed to install SIGTERM handler");
|
|
641
|
+
|
|
642
|
+
tokio::select! {
|
|
643
|
+
_ = sigint.recv() => "SIGINT",
|
|
644
|
+
_ = sigterm.recv() => "SIGTERM",
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
#[cfg(not(unix))]
|
|
649
|
+
async fn wait_for_shutdown_signal() -> &'static str {
|
|
650
|
+
let _ = tokio::signal::ctrl_c().await;
|
|
651
|
+
"Ctrl+C"
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
#[cfg(unix)]
|
|
655
|
+
async fn terminate_process_group_unix(pid: u32, label: &str) {
|
|
656
|
+
let process_group = pid as i32;
|
|
657
|
+
if process_group <= 0 {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
let terminate_result = unsafe { libc::killpg(process_group, libc::SIGTERM) };
|
|
662
|
+
if terminate_result != 0 {
|
|
663
|
+
let error = std::io::Error::last_os_error();
|
|
664
|
+
if error.raw_os_error() != Some(libc::ESRCH) {
|
|
665
|
+
eprintln!("failed to terminate {label} process group {process_group}: {error}");
|
|
666
|
+
}
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
tokio::time::sleep(Duration::from_millis(400)).await;
|
|
671
|
+
|
|
672
|
+
let kill_result = unsafe { libc::killpg(process_group, 0) };
|
|
673
|
+
if kill_result == 0 {
|
|
674
|
+
let force_result = unsafe { libc::killpg(process_group, libc::SIGKILL) };
|
|
675
|
+
if force_result != 0 {
|
|
676
|
+
let error = std::io::Error::last_os_error();
|
|
677
|
+
if error.raw_os_error() != Some(libc::ESRCH) {
|
|
678
|
+
eprintln!("failed to force-kill {label} process group {process_group}: {error}");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
#[cfg(windows)]
|
|
685
|
+
async fn terminate_process_tree_windows(pid: u32, label: &str) {
|
|
686
|
+
let status = Command::new("taskkill")
|
|
687
|
+
.arg("/PID")
|
|
688
|
+
.arg(pid.to_string())
|
|
689
|
+
.arg("/T")
|
|
690
|
+
.arg("/F")
|
|
691
|
+
.status()
|
|
692
|
+
.await;
|
|
693
|
+
|
|
694
|
+
match status {
|
|
695
|
+
Ok(result) if result.success() => {}
|
|
696
|
+
Ok(result) => eprintln!("failed to terminate {label} process tree {pid}: {result}"),
|
|
697
|
+
Err(error) => eprintln!("failed to terminate {label} process tree {pid}: {error}"),
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
603
701
|
enum RuntimeBackendRef<'a> {
|
|
604
702
|
Codex(&'a Arc<AppServerBridge>),
|
|
605
703
|
Opencode(&'a Arc<OpencodeBackend>),
|
|
@@ -773,6 +871,7 @@ impl ClientHub {
|
|
|
773
871
|
struct AppServerBridge {
|
|
774
872
|
engine: BridgeRuntimeEngine,
|
|
775
873
|
child: Mutex<Child>,
|
|
874
|
+
child_pid: u32,
|
|
776
875
|
writer: Mutex<ChildStdin>,
|
|
777
876
|
pending_requests: Mutex<HashMap<u64, PendingRequest>>,
|
|
778
877
|
internal_waiters: Mutex<HashMap<u64, oneshot::Sender<Result<Value, String>>>>,
|
|
@@ -815,15 +914,22 @@ impl AppServerBridge {
|
|
|
815
914
|
engine: BridgeRuntimeEngine,
|
|
816
915
|
hub: Arc<ClientHub>,
|
|
817
916
|
) -> Result<Arc<Self>, String> {
|
|
818
|
-
let mut
|
|
917
|
+
let mut command = Command::new(cli_bin);
|
|
918
|
+
command
|
|
819
919
|
.arg("app-server")
|
|
820
920
|
.arg("--listen")
|
|
821
921
|
.arg("stdio://")
|
|
822
922
|
.stdin(Stdio::piped())
|
|
823
923
|
.stdout(Stdio::piped())
|
|
824
|
-
.stderr(Stdio::piped())
|
|
924
|
+
.stderr(Stdio::piped());
|
|
925
|
+
configure_managed_child_command(&mut command);
|
|
926
|
+
|
|
927
|
+
let mut child = command
|
|
825
928
|
.spawn()
|
|
826
929
|
.map_err(|error| format!("failed to start app-server: {error}"))?;
|
|
930
|
+
let child_pid = child
|
|
931
|
+
.id()
|
|
932
|
+
.ok_or_else(|| "app-server pid unavailable".to_string())?;
|
|
827
933
|
|
|
828
934
|
let stdin = child
|
|
829
935
|
.stdin
|
|
@@ -841,6 +947,7 @@ impl AppServerBridge {
|
|
|
841
947
|
let bridge = Arc::new(Self {
|
|
842
948
|
engine,
|
|
843
949
|
child: Mutex::new(child),
|
|
950
|
+
child_pid,
|
|
844
951
|
writer: Mutex::new(stdin),
|
|
845
952
|
pending_requests: Mutex::new(HashMap::new()),
|
|
846
953
|
internal_waiters: Mutex::new(HashMap::new()),
|
|
@@ -861,6 +968,10 @@ impl AppServerBridge {
|
|
|
861
968
|
Ok(bridge)
|
|
862
969
|
}
|
|
863
970
|
|
|
971
|
+
async fn request_shutdown(&self) {
|
|
972
|
+
terminate_managed_child(self.child_pid, "app-server").await;
|
|
973
|
+
}
|
|
974
|
+
|
|
864
975
|
async fn initialize(&self) -> Result<(), String> {
|
|
865
976
|
let init_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
|
|
866
977
|
let (tx, rx) = oneshot::channel::<Result<Value, String>>();
|
|
@@ -1519,6 +1630,7 @@ struct OpencodePendingUserInputEntry {
|
|
|
1519
1630
|
|
|
1520
1631
|
struct OpencodeBackend {
|
|
1521
1632
|
child: Mutex<Child>,
|
|
1633
|
+
child_pid: u32,
|
|
1522
1634
|
hub: Arc<ClientHub>,
|
|
1523
1635
|
http: HttpClient,
|
|
1524
1636
|
base_url: Url,
|
|
@@ -1547,6 +1659,7 @@ impl OpencodeBackend {
|
|
|
1547
1659
|
.stdin(Stdio::null())
|
|
1548
1660
|
.stdout(Stdio::piped())
|
|
1549
1661
|
.stderr(Stdio::piped());
|
|
1662
|
+
configure_managed_child_command(&mut command);
|
|
1550
1663
|
|
|
1551
1664
|
if let Some(password) = config.opencode_server_password.as_deref() {
|
|
1552
1665
|
command.env("OPENCODE_SERVER_PASSWORD", password);
|
|
@@ -1556,6 +1669,9 @@ impl OpencodeBackend {
|
|
|
1556
1669
|
let mut child = command
|
|
1557
1670
|
.spawn()
|
|
1558
1671
|
.map_err(|error| format!("failed to start opencode serve: {error}"))?;
|
|
1672
|
+
let child_pid = child
|
|
1673
|
+
.id()
|
|
1674
|
+
.ok_or_else(|| "opencode pid unavailable".to_string())?;
|
|
1559
1675
|
|
|
1560
1676
|
let stdout = child
|
|
1561
1677
|
.stdout
|
|
@@ -1574,6 +1690,7 @@ impl OpencodeBackend {
|
|
|
1574
1690
|
|
|
1575
1691
|
let backend = Arc::new(Self {
|
|
1576
1692
|
child: Mutex::new(child),
|
|
1693
|
+
child_pid,
|
|
1577
1694
|
hub,
|
|
1578
1695
|
http: HttpClient::builder()
|
|
1579
1696
|
.build()
|
|
@@ -1600,6 +1717,10 @@ impl OpencodeBackend {
|
|
|
1600
1717
|
Ok(backend)
|
|
1601
1718
|
}
|
|
1602
1719
|
|
|
1720
|
+
async fn request_shutdown(&self) {
|
|
1721
|
+
terminate_managed_child(self.child_pid, "opencode").await;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1603
1724
|
fn spawn_stdout_loop(self: &Arc<Self>, stdout: ChildStdout) {
|
|
1604
1725
|
tokio::spawn(async move {
|
|
1605
1726
|
let mut lines = BufReader::new(stdout).lines();
|
|
@@ -4126,7 +4247,7 @@ async fn main() {
|
|
|
4126
4247
|
.route("/rpc", get(ws_handler))
|
|
4127
4248
|
.route("/health", get(health_handler))
|
|
4128
4249
|
.route("/local-image", get(local_image_handler))
|
|
4129
|
-
.with_state(state);
|
|
4250
|
+
.with_state(state.clone());
|
|
4130
4251
|
|
|
4131
4252
|
let bind_addr = format!("{}:{}", config.host, config.port);
|
|
4132
4253
|
let listener = match tokio::net::TcpListener::bind(&bind_addr).await {
|
|
@@ -4140,7 +4261,18 @@ async fn main() {
|
|
|
4140
4261
|
println!("rust-bridge listening on {bind_addr}");
|
|
4141
4262
|
maybe_print_pairing_qr(&config);
|
|
4142
4263
|
|
|
4143
|
-
|
|
4264
|
+
let shutdown_backend = state.backend.clone();
|
|
4265
|
+
let serve_result = axum::serve(listener, app)
|
|
4266
|
+
.with_graceful_shutdown(async move {
|
|
4267
|
+
let signal = wait_for_shutdown_signal().await;
|
|
4268
|
+
eprintln!("shutdown signal received ({signal}), terminating managed backends");
|
|
4269
|
+
shutdown_backend.shutdown().await;
|
|
4270
|
+
})
|
|
4271
|
+
.await;
|
|
4272
|
+
|
|
4273
|
+
state.backend.shutdown().await;
|
|
4274
|
+
|
|
4275
|
+
if let Err(error) = serve_result {
|
|
4144
4276
|
eprintln!("server error: {error}");
|
|
4145
4277
|
std::process::exit(1);
|
|
4146
4278
|
}
|
|
@@ -7373,6 +7505,7 @@ mod tests {
|
|
|
7373
7505
|
Arc::new(AppServerBridge {
|
|
7374
7506
|
engine: BridgeRuntimeEngine::Codex,
|
|
7375
7507
|
child: Mutex::new(child),
|
|
7508
|
+
child_pid: 0,
|
|
7376
7509
|
writer: Mutex::new(writer),
|
|
7377
7510
|
pending_requests: Mutex::new(HashMap::new()),
|
|
7378
7511
|
internal_waiters: Mutex::new(HashMap::new()),
|
|
@@ -7401,6 +7534,7 @@ mod tests {
|
|
|
7401
7534
|
|
|
7402
7535
|
Arc::new(OpencodeBackend {
|
|
7403
7536
|
child: Mutex::new(child),
|
|
7537
|
+
child_pid: 0,
|
|
7404
7538
|
hub,
|
|
7405
7539
|
http: HttpClient::builder().build().expect("build reqwest client"),
|
|
7406
7540
|
base_url: Url::parse("http://127.0.0.1:4090/").expect("valid opencode base url"),
|
|
@@ -8999,6 +9133,7 @@ mod tests {
|
|
|
8999
9133
|
let bridge = Arc::new(AppServerBridge {
|
|
9000
9134
|
engine: BridgeRuntimeEngine::Codex,
|
|
9001
9135
|
child: Mutex::new(child),
|
|
9136
|
+
child_pid: 0,
|
|
9002
9137
|
writer: Mutex::new(writer),
|
|
9003
9138
|
pending_requests: Mutex::new(HashMap::new()),
|
|
9004
9139
|
internal_waiters: Mutex::new(HashMap::new()),
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|