Skip to content

Commit 4bbba94

Browse files
committed
fix(desktop): delegate proxy lifecycle to core CLI and drop call-log start probe
1 parent 3bc65b4 commit 4bbba94

5 files changed

Lines changed: 42 additions & 174 deletions

File tree

core/internal/buildinfo/buildinfo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import "strings"
44

55
// Set at link time via -ldflags (see .goreleaser.yaml).
66
var (
7-
Version = "dev0.1.40"
7+
Version = "dev0.1.42"
88
Commit = "none"
99
Date = "unknown"
1010
)

electron/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "clovapi-switcher",
33
"private": true,
4-
"version": "0.1.7",
4+
"version": "0.1.8",
55
"description": "ClovAPI Switcher desktop app",
66
"main": "main.js",
77
"type": "commonjs",

electron/proxy-manager.js

Lines changed: 11 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -143,36 +143,6 @@ async function defaultFetchHealth(url) {
143143
}
144144
}
145145

146-
/** @param {ProxyBindConfig} cfg */
147-
function callLogUrl(cfg) {
148-
const { host, port } = buildProxyServeArgs(cfg);
149-
const clientHost = healthClientHost(host);
150-
return `http://${clientHost}:${port}/__debug/call-log`;
151-
}
152-
153-
/** @type {(url: string) => Promise<{ ok: boolean; supports: boolean; error?: string }>} */
154-
async function defaultFetchCallLogSupport(url) {
155-
const target = String(url || "").trim();
156-
if (!target) {
157-
return { ok: false, supports: false, error: "call-log URL is empty" };
158-
}
159-
const ac = new AbortController();
160-
const timer = setTimeout(() => ac.abort(), 2500);
161-
try {
162-
const res = await fetch(target, { signal: ac.signal });
163-
if (!res.ok) {
164-
return { ok: false, supports: false, error: `call-log status ${res.status}` };
165-
}
166-
const json = await res.json();
167-
return { ok: true, supports: Array.isArray(json?.entries) };
168-
} catch (e) {
169-
const msg = e instanceof Error ? e.message : "call-log request failed";
170-
return { ok: false, supports: false, error: msg };
171-
} finally {
172-
clearTimeout(timer);
173-
}
174-
}
175-
176146
/** @param {ProxyBindConfig} cfg @param {number} [skipPid] */
177147
function releaseBindPort(cfg, skipPid = 0) {
178148
const port = Number(cfg?.port) || DEFAULT_PORT;
@@ -212,7 +182,6 @@ function releaseBindPort(cfg, skipPid = 0) {
212182
* @property {() => Promise<string>} resolveExecutable Zero when missing.
213183
* @property {typeof spawn} [spawnFn]
214184
* @property {typeof defaultFetchHealth} [fetchHealth]
215-
* @property {typeof defaultFetchCallLogSupport} [fetchCallLogSupport]
216185
* @property {number} [healthPollMs]
217186
* @property {number} [healthDeadlineMs]
218187
* @property {() => Promise<{ host: string; port: number; enabled?: boolean }>} [loadProxyConfigFn]
@@ -225,7 +194,6 @@ function createGoProxyManager(deps) {
225194
const resolveExecutable = deps.resolveExecutable;
226195
const spawnFn = deps.spawnFn || spawn;
227196
const fetchHealth = deps.fetchHealth || defaultFetchHealth;
228-
const fetchCallLogSupport = deps.fetchCallLogSupport || defaultFetchCallLogSupport;
229197
const healthPollMs = Number(deps.healthPollMs) > 0 ? Number(deps.healthPollMs) : 80;
230198
const healthDeadlineMs = Number(deps.healthDeadlineMs) > 0 ? Number(deps.healthDeadlineMs) : 15_000;
231199

@@ -467,31 +435,7 @@ function createGoProxyManager(deps) {
467435
await killManagedSubtree(pid);
468436
}
469437

470-
/** @param {ProxyBindConfig} cfg */
471-
async function proxySupportsCallLog(cfg) {
472-
const result = await fetchCallLogSupport(callLogUrl(cfg));
473-
return Boolean(result.supports);
474-
}
475-
476-
/** @param {ProxyBindConfig} cfg @param {{ host: string; port: number; baseUrl: string }} urls */
477-
async function acceptExternalProxy(cfg, urls) {
478-
if (!(await proxySupportsCallLog(cfg))) {
479-
return null;
480-
}
481-
return {
482-
ok: true,
483-
alreadyRunning: true,
484-
running: true,
485-
managed: false,
486-
pid: null,
487-
external: true,
488-
port: urls.port,
489-
host: urls.host,
490-
baseUrl: urls.baseUrl,
491-
};
492-
}
493-
494-
/** @param {ProxyBindConfig} cfg @param {string} reason */
438+
/** @param {ProxyBindConfig} cfg @param {string} [_reason] */
495439
async function replaceStaleExternalProxy(cfg, _reason) {
496440
await stopProxyOnPort(cfg);
497441
await releaseBindPort(cfg, managedPidOrNull() ?? 0);
@@ -532,54 +476,7 @@ function createGoProxyManager(deps) {
532476
};
533477
const urls = snapshotBaseUrls(merged);
534478
let hProbe = await fetchHealth(healthUrl(merged));
535-
const ownedAlive = ownsHealthyManagedPid();
536-
537-
hProbe = await maybeReplaceStaleDevProxy(merged, hProbe, ownedAlive);
538-
539-
if (hProbe.ok && !ownedAlive) {
540-
const external = await acceptExternalProxy(merged, urls);
541-
if (external) return external;
542-
await replaceStaleExternalProxy(
543-
merged,
544-
"replacing stale external proxy without call-log support",
545-
);
546-
}
547-
548-
if (hProbe.ok && ownedAlive && managedPidOrNull() != null) {
549-
if (await proxySupportsCallLog(merged)) {
550-
return {
551-
ok: true,
552-
alreadyRunning: true,
553-
running: true,
554-
managed: true,
555-
pid: managedPidOrNull(),
556-
port: urls.port,
557-
host: urls.host,
558-
baseUrl: urls.baseUrl,
559-
};
560-
}
561-
await stopManaged("managed-proxy-without-call-log-support");
562-
await new Promise((r) => setTimeout(r, 140));
563-
}
564-
565-
if (ownedAlive && !hProbe.ok) {
566-
try {
567-
await waitForHealthy(merged);
568-
return {
569-
ok: true,
570-
alreadyRunning: true,
571-
running: true,
572-
managed: true,
573-
pid: managedPidOrNull(),
574-
port: urls.port,
575-
host: urls.host,
576-
baseUrl: urls.baseUrl,
577-
};
578-
} catch {
579-
await stopManaged("unhealthy-managed-child");
580-
await new Promise((r) => setTimeout(r, 140));
581-
}
582-
}
479+
hProbe = await maybeReplaceStaleDevProxy(merged, hProbe, false);
583480

584481
const exe = await resolveExecutable();
585482
if (!exe) {
@@ -598,15 +495,10 @@ function createGoProxyManager(deps) {
598495

599496
try {
600497
await launchProxyDaemon(exe, merged);
601-
await waitForHealthy(merged);
602498
} catch (e) {
603499
consecutiveStartFailures += 1;
604500
const errMsg = e instanceof Error ? e.message : String(e);
605501
const hc = await fetchHealth(healthUrl(merged));
606-
if (hc.ok === true) {
607-
const external = await acceptExternalProxy(merged, urls);
608-
if (external) return { ...external, alreadyRunning: false };
609-
}
610502
return {
611503
ok: false,
612504
running: Boolean(hc.ok),
@@ -620,19 +512,18 @@ function createGoProxyManager(deps) {
620512
};
621513
}
622514

623-
const external = await acceptExternalProxy(merged, urls);
624-
if (external) {
625-
return { ...external, alreadyRunning: false };
626-
}
515+
const hc = await fetchHealth(healthUrl(merged));
627516
return {
628-
ok: false,
629-
running: true,
517+
ok: hc.ok === true,
518+
alreadyRunning: Boolean(hProbe.ok),
519+
running: hc.ok === true,
630520
managed: false,
631521
pid: null,
522+
external: hc.ok === true,
632523
port: urls.port,
633524
host: urls.host,
634525
baseUrl: urls.baseUrl,
635-
error: "proxy started but call-log support probe failed",
526+
error: hc.ok ? "" : String(hc.error || "proxy health check failed after start"),
636527
};
637528
}
638529

@@ -687,40 +578,6 @@ function createGoProxyManager(deps) {
687578
}
688579

689580
async function ensureRunning(payload = {}) {
690-
const base = await loadProxyConfig();
691-
const mergedCfg = {
692-
...base,
693-
...(Number(payload.port) > 0 ? { port: Number(payload.port) } : {}),
694-
};
695-
const urls = snapshotBaseUrls(mergedCfg);
696-
let hi = await fetchHealth(healthUrl(mergedCfg));
697-
if (hi.ok === true) {
698-
const owns = ownsHealthyManagedPid();
699-
hi = await maybeReplaceStaleDevProxy(mergedCfg, hi, owns);
700-
}
701-
if (hi.ok === true) {
702-
const owns = ownsHealthyManagedPid();
703-
if (await proxySupportsCallLog(mergedCfg)) {
704-
return {
705-
ok: true,
706-
running: true,
707-
managed: Boolean(owns),
708-
pid: managedPidOrNull(),
709-
port: urls.port,
710-
host: urls.host,
711-
baseUrl: urls.baseUrl,
712-
};
713-
}
714-
if (owns) {
715-
await stopManaged("managed-proxy-without-call-log-support");
716-
await new Promise((resolve) => setTimeout(resolve, 140));
717-
} else {
718-
await replaceStaleExternalProxy(
719-
mergedCfg,
720-
"replacing stale external proxy without call-log support",
721-
);
722-
}
723-
}
724581
const started = await start(payload);
725582
if (!started.ok) {
726583
return started;
@@ -730,9 +587,9 @@ function createGoProxyManager(deps) {
730587
running: Boolean(started.running),
731588
managed: Boolean(started.managed),
732589
pid: started.pid ?? null,
733-
port: started.port ?? urls.port,
734-
host: started.host ?? urls.host,
735-
baseUrl: started.baseUrl ?? urls.baseUrl,
590+
port: started.port,
591+
host: started.host,
592+
baseUrl: started.baseUrl,
736593
};
737594
}
738595

@@ -760,9 +617,7 @@ module.exports = {
760617
buildProxyStopArgs,
761618
normalizeBindHost,
762619
healthClientHost,
763-
reachableLoopbackHost,
764620
healthUrl,
765-
callLogUrl,
766621
redactSecrets,
767622
createGoProxyManager,
768623
};

electron/proxy-manager.test.js

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,37 @@ test("redactSecrets masks bearer-ish tokens", () => {
5656
assert.match(out, /\[redacted\]/);
5757
});
5858

59-
test("start — external proxy: no spawn when /health OK", async () => {
59+
test("start — invokes proxy start even when /health already OK", async () => {
60+
let sawSpawn = false;
61+
class FakeProcess extends EventEmitter {
62+
stdout = new EventEmitter();
63+
stderr = new EventEmitter();
64+
pid = 42_002;
65+
killed = false;
66+
exitCode = null;
67+
signalCode = null;
68+
kill() {
69+
/* keep alive until test ends */
70+
}
71+
}
72+
73+
const fakeChild = new FakeProcess();
6074
const mgr = createGoProxyManager({
61-
resolveExecutable: async () => "should-not-run",
75+
resolveExecutable: async () => "/opt/bin/clovapi",
6276
loadProxyConfigFn: async () => ({ host: "127.0.0.1", port: 57901 }),
63-
spawnFn() {
64-
assert.fail("spawn must not be called when proxy already serves /health");
65-
},
6677
fetchHealth: async () => ({ ok: true, body: { ok: true, service: "clovapi-core-proxy" } }),
67-
fetchCallLogSupport: async () => ({ ok: true, supports: true }),
78+
spawnFn(executable, args) {
79+
sawSpawn = true;
80+
assert.equal(executable, "/opt/bin/clovapi");
81+
assert.deepEqual(args, ["proxy", "start", "--host", "127.0.0.1", "--port", "57901"]);
82+
queueMicrotask(() => {
83+
fakeChild.emit("close", 0, null);
84+
});
85+
return /** @type {any} */ (fakeChild);
86+
},
6887
});
6988
const st = await mgr.start({ port: 57901 });
89+
assert.ok(sawSpawn);
7090
assert.equal(st.ok, true);
7191
assert.equal(st.running, true);
7292
assert.equal(st.managed, false);
@@ -89,17 +109,10 @@ test("start — invokes spawnFn with proxy start argv", async () => {
89109
}
90110

91111
const fakeChild = new FakeProcess();
92-
let tick = 0;
93112
const mgr = createGoProxyManager({
94113
resolveExecutable: async () => "/opt/bin/clovapi",
95114
loadProxyConfigFn: async () => ({ host: "127.0.0.1", port: 58901 }),
96-
healthPollMs: 1,
97-
healthDeadlineMs: 2000,
98-
fetchHealth: async () => {
99-
tick += 1;
100-
return { ok: tick >= 3, body: tick >= 3 ? { ok: true, service: "clovapi-core-proxy" } : {} };
101-
},
102-
fetchCallLogSupport: async () => ({ ok: true, supports: true }),
115+
fetchHealth: async () => ({ ok: true, body: { ok: true, service: "clovapi-core-proxy" } }),
103116
spawnFn(executable, args) {
104117
sawSpawn = true;
105118
assert.equal(executable, "/opt/bin/clovapi");

0 commit comments

Comments
 (0)