@portel/photon 1.32.2 → 1.32.4
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 +8 -4
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +56 -14
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.js +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +5 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +452 -175
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +34 -0
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js +57 -0
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +2492 -1442
- package/dist/beam.bundle.js.map +4 -4
- package/dist/claude-code-plugin.js +9 -3
- package/dist/claude-code-plugin.js.map +1 -1
- package/dist/cli/commands/beam.d.ts.map +1 -1
- package/dist/cli/commands/beam.js +5 -0
- package/dist/cli/commands/beam.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +12 -6
- package/dist/context.js.map +1 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +187 -489
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +2 -1
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +57 -29
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/server.js +120 -31
- package/dist/daemon/server.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +19 -8
- package/dist/loader.js.map +1 -1
- package/dist/photons/marketplace.photon.d.ts.map +1 -1
- package/dist/photons/marketplace.photon.js +34 -7
- package/dist/photons/marketplace.photon.js.map +1 -1
- package/dist/photons/marketplace.photon.ts +35 -7
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +40 -6
- package/dist/server.js.map +1 -1
- package/dist/types/server-types.d.ts +2 -0
- package/dist/types/server-types.d.ts.map +1 -1
- package/dist/version-notify.d.ts +5 -0
- package/dist/version-notify.d.ts.map +1 -1
- package/dist/version-notify.js +57 -7
- package/dist/version-notify.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +8 -3
- package/dist/watcher.js.map +1 -1
- package/package.json +89 -73
package/dist/daemon/client.js
CHANGED
|
@@ -18,6 +18,12 @@ import { ProgressRenderer } from '@portel/photon-core';
|
|
|
18
18
|
const SESSION_ID = process.env.PHOTON_SESSION_ID || `cli-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
19
19
|
const logger = createLogger({ component: 'daemon-client', minimal: true });
|
|
20
20
|
const cliProgress = new ProgressRenderer();
|
|
21
|
+
const DEFAULT_DAEMON_READY_TIMEOUT_MS = 30_000;
|
|
22
|
+
const DEFAULT_DAEMON_COMMAND_RETRIES = 3;
|
|
23
|
+
function getDaemonReadyTimeoutMs() {
|
|
24
|
+
const parsed = Number(process.env.PHOTON_DAEMON_READY_TIMEOUT_MS);
|
|
25
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
26
|
+
}
|
|
21
27
|
/**
|
|
22
28
|
* Render a generator emit yield in the CLI (mirrors loader's createOutputHandler)
|
|
23
29
|
*/
|
|
@@ -154,16 +160,19 @@ function connectToDaemon(socketPath) {
|
|
|
154
160
|
process.nextTick(() => dummy.emit('error', err));
|
|
155
161
|
return dummy;
|
|
156
162
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
const socket = new net.Socket();
|
|
164
|
+
// Give callers one tick to attach error/data/connect handlers before
|
|
165
|
+
// Bun can report ENOENT/ECONNREFUSED for a socket that vanished after
|
|
166
|
+
// the existsSync guard.
|
|
167
|
+
process.nextTick(() => {
|
|
168
|
+
try {
|
|
169
|
+
socket.connect(socketPath);
|
|
170
|
+
}
|
|
171
|
+
catch (syncErr) {
|
|
172
|
+
socket.emit('error', syncErr);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return socket;
|
|
167
176
|
}
|
|
168
177
|
/**
|
|
169
178
|
* connectToDaemon variant for the best-effort health-check helpers
|
|
@@ -206,7 +215,7 @@ const RETRYABLE_METHODS = new Set(['_instances', '_use', '_settings', '_runs', '
|
|
|
206
215
|
* Called after ensureDaemon() so sendCommand retries land on a ready socket,
|
|
207
216
|
* not on a socket that exists on disk but the daemon hasn't bound yet.
|
|
208
217
|
*/
|
|
209
|
-
async function waitForDaemon(timeoutMs =
|
|
218
|
+
async function waitForDaemon(timeoutMs = getDaemonReadyTimeoutMs()) {
|
|
210
219
|
const deadline = Date.now() + timeoutMs;
|
|
211
220
|
let delay = 100;
|
|
212
221
|
while (Date.now() < deadline) {
|
|
@@ -215,7 +224,15 @@ async function waitForDaemon(timeoutMs = 10_000) {
|
|
|
215
224
|
await new Promise((r) => setTimeout(r, delay));
|
|
216
225
|
delay = Math.min(delay * 2, 1000);
|
|
217
226
|
}
|
|
218
|
-
throw new Error(
|
|
227
|
+
throw new Error(`Daemon did not become ready within ${Math.round(timeoutMs / 1000)}s`);
|
|
228
|
+
}
|
|
229
|
+
async function ensureDaemonReady() {
|
|
230
|
+
await ensureDaemon();
|
|
231
|
+
await waitForDaemon();
|
|
232
|
+
}
|
|
233
|
+
async function waitBeforeDaemonRetry(attempt) {
|
|
234
|
+
const delay = Math.min(100 * Math.pow(2, attempt), 1_000);
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
219
236
|
}
|
|
220
237
|
/**
|
|
221
238
|
* Send command to daemon with auto-restart on connection failure.
|
|
@@ -223,17 +240,18 @@ async function waitForDaemon(timeoutMs = 10_000) {
|
|
|
223
240
|
* errors for read-only methods (retry without restart).
|
|
224
241
|
*/
|
|
225
242
|
export async function sendCommand(photonName, method, args, options) {
|
|
226
|
-
const maxRetries = options?.maxRetries ??
|
|
243
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_DAEMON_COMMAND_RETRIES;
|
|
227
244
|
const isRetryable = RETRYABLE_METHODS.has(method);
|
|
228
245
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
229
246
|
try {
|
|
247
|
+
await ensureDaemonReady();
|
|
230
248
|
return await sendCommandDirect(photonName, method, args, options?.photonPath, options?.sessionId, options?.instanceName, options?.workingDir, options?.targetInstance, options?.clientType);
|
|
231
249
|
}
|
|
232
250
|
catch (error) {
|
|
233
251
|
if (isDaemonConnectionError(error) && attempt < maxRetries) {
|
|
234
252
|
logger.info(`Daemon unreachable (${photonName}/${method}), waiting for daemon...`);
|
|
235
|
-
await
|
|
236
|
-
await
|
|
253
|
+
await waitBeforeDaemonRetry(attempt);
|
|
254
|
+
await ensureDaemonReady();
|
|
237
255
|
continue;
|
|
238
256
|
}
|
|
239
257
|
// Retry transient errors for read-only methods only
|
|
@@ -252,6 +270,24 @@ export async function sendCommand(photonName, method, args, options) {
|
|
|
252
270
|
* Called by Beam after hot-reload so the daemon's instance matches.
|
|
253
271
|
*/
|
|
254
272
|
export async function reloadDaemonPhoton(photonName, photonPath, workingDir) {
|
|
273
|
+
const maxRetries = DEFAULT_DAEMON_COMMAND_RETRIES;
|
|
274
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
275
|
+
try {
|
|
276
|
+
await ensureDaemonReady();
|
|
277
|
+
return await reloadDaemonPhotonDirect(photonName, photonPath, workingDir);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (isDaemonConnectionError(error) && attempt < maxRetries) {
|
|
281
|
+
logger.info(`Daemon unreachable during reload (${photonName}), waiting for daemon...`);
|
|
282
|
+
await waitBeforeDaemonRetry(attempt);
|
|
283
|
+
await ensureDaemonReady();
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function reloadDaemonPhotonDirect(photonName, photonPath, workingDir) {
|
|
255
291
|
const socketPath = getGlobalSocketPath();
|
|
256
292
|
return new Promise((resolve, reject) => {
|
|
257
293
|
const client = connectToDaemon(socketPath);
|
|
@@ -413,7 +449,8 @@ async function sendCommandDirect(photonName, method, args, photonPath, sessionId
|
|
|
413
449
|
export async function subscribeChannel(photonName, channel, handler, options) {
|
|
414
450
|
let cancelled = false;
|
|
415
451
|
let lastSeenEventId = options?.lastEventId;
|
|
416
|
-
const connect = () => {
|
|
452
|
+
const connect = async () => {
|
|
453
|
+
await ensureDaemonReady();
|
|
417
454
|
const socketPath = getGlobalSocketPath();
|
|
418
455
|
const subscribeId = `sub_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
419
456
|
return new Promise((resolve, reject) => {
|
|
@@ -511,6 +548,8 @@ export async function subscribeChannel(photonName, channel, handler, options) {
|
|
|
511
548
|
}
|
|
512
549
|
}
|
|
513
550
|
else if (options?.reconnect && !cancelled) {
|
|
551
|
+
if (!client.destroyed)
|
|
552
|
+
client.destroy();
|
|
514
553
|
scheduleReconnect();
|
|
515
554
|
}
|
|
516
555
|
});
|
|
@@ -527,6 +566,8 @@ export async function subscribeChannel(photonName, channel, handler, options) {
|
|
|
527
566
|
}
|
|
528
567
|
}
|
|
529
568
|
else if (options?.reconnect && !cancelled) {
|
|
569
|
+
if (!client.destroyed)
|
|
570
|
+
client.destroy();
|
|
530
571
|
scheduleReconnect();
|
|
531
572
|
}
|
|
532
573
|
});
|
|
@@ -551,7 +592,7 @@ export async function subscribeChannel(photonName, channel, handler, options) {
|
|
|
551
592
|
return;
|
|
552
593
|
try {
|
|
553
594
|
// Try reconnecting to existing daemon first; only start if it's down
|
|
554
|
-
await
|
|
595
|
+
await ensureDaemonReady();
|
|
555
596
|
await connect();
|
|
556
597
|
reconnectAttempts = 0;
|
|
557
598
|
logger.debug(`Reconnected subscription for ${channel}`);
|
|
@@ -573,48 +614,12 @@ export async function subscribeChannel(photonName, channel, handler, options) {
|
|
|
573
614
|
* Publish a message to a channel on a daemon
|
|
574
615
|
*/
|
|
575
616
|
export async function publishToChannel(photonName, channel, message, workingDir) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
reject(new Error('Publish timeout'));
|
|
583
|
-
}, 5000);
|
|
584
|
-
client.on('connect', () => {
|
|
585
|
-
const request = {
|
|
586
|
-
type: 'publish',
|
|
587
|
-
id: requestId,
|
|
588
|
-
photonName,
|
|
589
|
-
channel,
|
|
590
|
-
message,
|
|
591
|
-
workingDir,
|
|
592
|
-
};
|
|
593
|
-
client.write(JSON.stringify(request) + '\n');
|
|
594
|
-
});
|
|
595
|
-
client.on('data', (chunk) => {
|
|
596
|
-
try {
|
|
597
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
598
|
-
if (response.id === requestId) {
|
|
599
|
-
clearTimeout(timeout);
|
|
600
|
-
client.destroy();
|
|
601
|
-
if (response.type === 'result') {
|
|
602
|
-
resolve();
|
|
603
|
-
}
|
|
604
|
-
else {
|
|
605
|
-
reject(new Error(response.error || 'Publish failed'));
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
catch (e) {
|
|
610
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
client.on('error', (error) => {
|
|
614
|
-
clearTimeout(timeout);
|
|
615
|
-
client.destroy();
|
|
616
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
617
|
-
});
|
|
617
|
+
await sendSimpleDaemonRequest({
|
|
618
|
+
type: 'publish',
|
|
619
|
+
photonName,
|
|
620
|
+
channel,
|
|
621
|
+
message,
|
|
622
|
+
workingDir,
|
|
618
623
|
});
|
|
619
624
|
}
|
|
620
625
|
/**
|
|
@@ -622,485 +627,152 @@ export async function publishToChannel(photonName, channel, message, workingDir)
|
|
|
622
627
|
* Returns true if lock acquired, false if already held
|
|
623
628
|
*/
|
|
624
629
|
export async function acquireLock(photonName, lockName, timeout, workingDir) {
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}, 10000);
|
|
633
|
-
client.on('connect', () => {
|
|
634
|
-
const request = {
|
|
635
|
-
type: 'lock',
|
|
636
|
-
id: requestId,
|
|
637
|
-
photonName,
|
|
638
|
-
sessionId: SESSION_ID,
|
|
639
|
-
lockName,
|
|
640
|
-
lockTimeout: timeout,
|
|
641
|
-
workingDir,
|
|
642
|
-
};
|
|
643
|
-
client.write(JSON.stringify(request) + '\n');
|
|
644
|
-
});
|
|
645
|
-
client.on('data', (chunk) => {
|
|
646
|
-
try {
|
|
647
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
648
|
-
if (response.id === requestId) {
|
|
649
|
-
clearTimeout(requestTimeout);
|
|
650
|
-
client.destroy();
|
|
651
|
-
if (response.type === 'result') {
|
|
652
|
-
resolve(response.data.acquired);
|
|
653
|
-
}
|
|
654
|
-
else {
|
|
655
|
-
reject(new Error(response.error || 'Lock failed'));
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
catch (e) {
|
|
660
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
661
|
-
}
|
|
662
|
-
});
|
|
663
|
-
client.on('error', (error) => {
|
|
664
|
-
clearTimeout(requestTimeout);
|
|
665
|
-
client.destroy();
|
|
666
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
667
|
-
});
|
|
630
|
+
const result = await sendSimpleDaemonRequest({
|
|
631
|
+
type: 'lock',
|
|
632
|
+
photonName,
|
|
633
|
+
sessionId: SESSION_ID,
|
|
634
|
+
lockName,
|
|
635
|
+
lockTimeout: timeout,
|
|
636
|
+
workingDir,
|
|
668
637
|
});
|
|
638
|
+
return result.acquired;
|
|
669
639
|
}
|
|
670
640
|
/**
|
|
671
641
|
* Release a distributed lock
|
|
672
642
|
* Returns true if lock released, false if not held by this session
|
|
673
643
|
*/
|
|
674
644
|
export async function releaseLock(photonName, lockName, workingDir) {
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
reject(new Error('Unlock request timeout'));
|
|
682
|
-
}, 5000);
|
|
683
|
-
client.on('connect', () => {
|
|
684
|
-
const request = {
|
|
685
|
-
type: 'unlock',
|
|
686
|
-
id: requestId,
|
|
687
|
-
photonName,
|
|
688
|
-
sessionId: SESSION_ID,
|
|
689
|
-
lockName,
|
|
690
|
-
workingDir,
|
|
691
|
-
};
|
|
692
|
-
client.write(JSON.stringify(request) + '\n');
|
|
693
|
-
});
|
|
694
|
-
client.on('data', (chunk) => {
|
|
695
|
-
try {
|
|
696
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
697
|
-
if (response.id === requestId) {
|
|
698
|
-
clearTimeout(timeout);
|
|
699
|
-
client.destroy();
|
|
700
|
-
if (response.type === 'result') {
|
|
701
|
-
resolve(response.data.released);
|
|
702
|
-
}
|
|
703
|
-
else {
|
|
704
|
-
reject(new Error(response.error || 'Unlock failed'));
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
catch (e) {
|
|
709
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
client.on('error', (error) => {
|
|
713
|
-
clearTimeout(timeout);
|
|
714
|
-
client.destroy();
|
|
715
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
716
|
-
});
|
|
645
|
+
const result = await sendSimpleDaemonRequest({
|
|
646
|
+
type: 'unlock',
|
|
647
|
+
photonName,
|
|
648
|
+
sessionId: SESSION_ID,
|
|
649
|
+
lockName,
|
|
650
|
+
workingDir,
|
|
717
651
|
});
|
|
652
|
+
return result.released;
|
|
718
653
|
}
|
|
719
654
|
/**
|
|
720
655
|
* List all active locks
|
|
721
656
|
*/
|
|
722
657
|
export async function listLocks(photonName) {
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const client = connectToDaemon(socketPath);
|
|
727
|
-
const timeout = setTimeout(() => {
|
|
728
|
-
client.destroy();
|
|
729
|
-
reject(new Error('List locks request timeout'));
|
|
730
|
-
}, 5000);
|
|
731
|
-
client.on('connect', () => {
|
|
732
|
-
const request = {
|
|
733
|
-
type: 'list_locks',
|
|
734
|
-
id: requestId,
|
|
735
|
-
photonName,
|
|
736
|
-
};
|
|
737
|
-
client.write(JSON.stringify(request) + '\n');
|
|
738
|
-
});
|
|
739
|
-
client.on('data', (chunk) => {
|
|
740
|
-
try {
|
|
741
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
742
|
-
if (response.id === requestId) {
|
|
743
|
-
clearTimeout(timeout);
|
|
744
|
-
client.destroy();
|
|
745
|
-
if (response.type === 'result') {
|
|
746
|
-
resolve(response.data.locks || []);
|
|
747
|
-
}
|
|
748
|
-
else {
|
|
749
|
-
reject(new Error(response.error || 'List locks failed'));
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
catch (e) {
|
|
754
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
755
|
-
}
|
|
756
|
-
});
|
|
757
|
-
client.on('error', (error) => {
|
|
758
|
-
clearTimeout(timeout);
|
|
759
|
-
client.destroy();
|
|
760
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
761
|
-
});
|
|
658
|
+
const result = await sendSimpleDaemonRequest({
|
|
659
|
+
type: 'list_locks',
|
|
660
|
+
photonName,
|
|
762
661
|
});
|
|
662
|
+
return result.locks || [];
|
|
763
663
|
}
|
|
764
664
|
/**
|
|
765
665
|
* Assign a lock to a specific caller (identity-aware)
|
|
766
666
|
* Unlike acquireLock which uses the session ID, this sets an explicit holder.
|
|
767
667
|
*/
|
|
768
668
|
export async function assignLock(photonName, lockName, holder, timeout, workingDir) {
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
client.on('connect', () => {
|
|
778
|
-
const request = {
|
|
779
|
-
type: 'assign_lock',
|
|
780
|
-
id: requestId,
|
|
781
|
-
photonName,
|
|
782
|
-
sessionId: SESSION_ID,
|
|
783
|
-
lockName,
|
|
784
|
-
lockHolder: holder,
|
|
785
|
-
lockTimeout: timeout,
|
|
786
|
-
workingDir,
|
|
787
|
-
};
|
|
788
|
-
client.write(JSON.stringify(request) + '\n');
|
|
789
|
-
});
|
|
790
|
-
client.on('data', (chunk) => {
|
|
791
|
-
try {
|
|
792
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
793
|
-
if (response.id === requestId) {
|
|
794
|
-
clearTimeout(requestTimeout);
|
|
795
|
-
client.destroy();
|
|
796
|
-
if (response.type === 'result') {
|
|
797
|
-
resolve(response.data.acquired);
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
reject(new Error(response.error || 'Assign lock failed'));
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
catch (e) {
|
|
805
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
806
|
-
}
|
|
807
|
-
});
|
|
808
|
-
client.on('error', (error) => {
|
|
809
|
-
clearTimeout(requestTimeout);
|
|
810
|
-
client.destroy();
|
|
811
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
812
|
-
});
|
|
669
|
+
const result = await sendSimpleDaemonRequest({
|
|
670
|
+
type: 'assign_lock',
|
|
671
|
+
photonName,
|
|
672
|
+
sessionId: SESSION_ID,
|
|
673
|
+
lockName,
|
|
674
|
+
lockHolder: holder,
|
|
675
|
+
lockTimeout: timeout,
|
|
676
|
+
workingDir,
|
|
813
677
|
});
|
|
678
|
+
return result.acquired;
|
|
814
679
|
}
|
|
815
680
|
/**
|
|
816
681
|
* Transfer a lock from one holder to another
|
|
817
682
|
*/
|
|
818
683
|
export async function transferLock(photonName, lockName, fromHolder, toHolder, timeout, workingDir) {
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const request = {
|
|
829
|
-
type: 'transfer_lock',
|
|
830
|
-
id: requestId,
|
|
831
|
-
photonName,
|
|
832
|
-
sessionId: SESSION_ID,
|
|
833
|
-
lockName,
|
|
834
|
-
lockHolder: fromHolder,
|
|
835
|
-
lockTransferTo: toHolder,
|
|
836
|
-
lockTimeout: timeout,
|
|
837
|
-
workingDir,
|
|
838
|
-
};
|
|
839
|
-
client.write(JSON.stringify(request) + '\n');
|
|
840
|
-
});
|
|
841
|
-
client.on('data', (chunk) => {
|
|
842
|
-
try {
|
|
843
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
844
|
-
if (response.id === requestId) {
|
|
845
|
-
clearTimeout(requestTimeout);
|
|
846
|
-
client.destroy();
|
|
847
|
-
if (response.type === 'result') {
|
|
848
|
-
resolve(response.data.transferred);
|
|
849
|
-
}
|
|
850
|
-
else {
|
|
851
|
-
reject(new Error(response.error || 'Transfer lock failed'));
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
catch (e) {
|
|
856
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
857
|
-
}
|
|
858
|
-
});
|
|
859
|
-
client.on('error', (error) => {
|
|
860
|
-
clearTimeout(requestTimeout);
|
|
861
|
-
client.destroy();
|
|
862
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
863
|
-
});
|
|
684
|
+
const result = await sendSimpleDaemonRequest({
|
|
685
|
+
type: 'transfer_lock',
|
|
686
|
+
photonName,
|
|
687
|
+
sessionId: SESSION_ID,
|
|
688
|
+
lockName,
|
|
689
|
+
lockHolder: fromHolder,
|
|
690
|
+
lockTransferTo: toHolder,
|
|
691
|
+
lockTimeout: timeout,
|
|
692
|
+
workingDir,
|
|
864
693
|
});
|
|
694
|
+
return result.transferred;
|
|
865
695
|
}
|
|
866
696
|
/**
|
|
867
697
|
* Release a lock held by a specific caller (identity-aware)
|
|
868
698
|
*/
|
|
869
699
|
export async function releaseIdentityLock(photonName, lockName, holder, workingDir) {
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}, 5000);
|
|
878
|
-
client.on('connect', () => {
|
|
879
|
-
const request = {
|
|
880
|
-
type: 'unlock',
|
|
881
|
-
id: requestId,
|
|
882
|
-
photonName,
|
|
883
|
-
sessionId: SESSION_ID,
|
|
884
|
-
lockName,
|
|
885
|
-
lockHolder: holder,
|
|
886
|
-
workingDir,
|
|
887
|
-
};
|
|
888
|
-
client.write(JSON.stringify(request) + '\n');
|
|
889
|
-
});
|
|
890
|
-
client.on('data', (chunk) => {
|
|
891
|
-
try {
|
|
892
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
893
|
-
if (response.id === requestId) {
|
|
894
|
-
clearTimeout(requestTimeout);
|
|
895
|
-
client.destroy();
|
|
896
|
-
if (response.type === 'result') {
|
|
897
|
-
resolve(response.data.released);
|
|
898
|
-
}
|
|
899
|
-
else {
|
|
900
|
-
reject(new Error(response.error || 'Release lock failed'));
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
catch (e) {
|
|
905
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
906
|
-
}
|
|
907
|
-
});
|
|
908
|
-
client.on('error', (error) => {
|
|
909
|
-
clearTimeout(requestTimeout);
|
|
910
|
-
client.destroy();
|
|
911
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
912
|
-
});
|
|
700
|
+
const result = await sendSimpleDaemonRequest({
|
|
701
|
+
type: 'unlock',
|
|
702
|
+
photonName,
|
|
703
|
+
sessionId: SESSION_ID,
|
|
704
|
+
lockName,
|
|
705
|
+
lockHolder: holder,
|
|
706
|
+
workingDir,
|
|
913
707
|
});
|
|
708
|
+
return result.released;
|
|
914
709
|
}
|
|
915
710
|
/**
|
|
916
711
|
* Query who holds a specific lock
|
|
917
712
|
*/
|
|
918
713
|
export async function queryLock(photonName, lockName) {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
const requestTimeout = setTimeout(() => {
|
|
924
|
-
client.destroy();
|
|
925
|
-
reject(new Error('Query lock request timeout'));
|
|
926
|
-
}, 5000);
|
|
927
|
-
client.on('connect', () => {
|
|
928
|
-
const request = {
|
|
929
|
-
type: 'query_lock',
|
|
930
|
-
id: requestId,
|
|
931
|
-
photonName,
|
|
932
|
-
lockName,
|
|
933
|
-
};
|
|
934
|
-
client.write(JSON.stringify(request) + '\n');
|
|
935
|
-
});
|
|
936
|
-
client.on('data', (chunk) => {
|
|
937
|
-
try {
|
|
938
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
939
|
-
if (response.id === requestId) {
|
|
940
|
-
clearTimeout(requestTimeout);
|
|
941
|
-
client.destroy();
|
|
942
|
-
if (response.type === 'result') {
|
|
943
|
-
resolve(response.data);
|
|
944
|
-
}
|
|
945
|
-
else {
|
|
946
|
-
reject(new Error(response.error || 'Query lock failed'));
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
catch (e) {
|
|
951
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
client.on('error', (error) => {
|
|
955
|
-
clearTimeout(requestTimeout);
|
|
956
|
-
client.destroy();
|
|
957
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
958
|
-
});
|
|
714
|
+
return sendSimpleDaemonRequest({
|
|
715
|
+
type: 'query_lock',
|
|
716
|
+
photonName,
|
|
717
|
+
lockName,
|
|
959
718
|
});
|
|
960
719
|
}
|
|
961
720
|
/**
|
|
962
721
|
* Schedule a recurring job
|
|
963
722
|
*/
|
|
964
723
|
export async function scheduleJob(photonName, jobId, method, cron, args) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
client.on('connect', () => {
|
|
974
|
-
const request = {
|
|
975
|
-
type: 'schedule',
|
|
976
|
-
id: requestId,
|
|
977
|
-
photonName,
|
|
978
|
-
sessionId: SESSION_ID,
|
|
979
|
-
jobId,
|
|
980
|
-
method,
|
|
981
|
-
cron,
|
|
982
|
-
args,
|
|
983
|
-
};
|
|
984
|
-
client.write(JSON.stringify(request) + '\n');
|
|
985
|
-
});
|
|
986
|
-
client.on('data', (chunk) => {
|
|
987
|
-
try {
|
|
988
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
989
|
-
if (response.id === requestId) {
|
|
990
|
-
clearTimeout(timeout);
|
|
991
|
-
client.destroy();
|
|
992
|
-
if (response.type === 'result') {
|
|
993
|
-
resolve(response.data);
|
|
994
|
-
}
|
|
995
|
-
else {
|
|
996
|
-
reject(new Error(response.error || 'Schedule failed'));
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
catch (e) {
|
|
1001
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
1002
|
-
}
|
|
1003
|
-
});
|
|
1004
|
-
client.on('error', (error) => {
|
|
1005
|
-
clearTimeout(timeout);
|
|
1006
|
-
client.destroy();
|
|
1007
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
1008
|
-
});
|
|
724
|
+
return sendSimpleDaemonRequest({
|
|
725
|
+
type: 'schedule',
|
|
726
|
+
photonName,
|
|
727
|
+
sessionId: SESSION_ID,
|
|
728
|
+
jobId,
|
|
729
|
+
method,
|
|
730
|
+
cron,
|
|
731
|
+
args,
|
|
1009
732
|
});
|
|
1010
733
|
}
|
|
1011
734
|
/**
|
|
1012
735
|
* Unschedule a job
|
|
1013
736
|
*/
|
|
1014
737
|
export async function unscheduleJob(photonName, jobId) {
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
const timeout = setTimeout(() => {
|
|
1020
|
-
client.destroy();
|
|
1021
|
-
reject(new Error('Unschedule request timeout'));
|
|
1022
|
-
}, 5000);
|
|
1023
|
-
client.on('connect', () => {
|
|
1024
|
-
const request = {
|
|
1025
|
-
type: 'unschedule',
|
|
1026
|
-
id: requestId,
|
|
1027
|
-
photonName,
|
|
1028
|
-
jobId,
|
|
1029
|
-
};
|
|
1030
|
-
client.write(JSON.stringify(request) + '\n');
|
|
1031
|
-
});
|
|
1032
|
-
client.on('data', (chunk) => {
|
|
1033
|
-
try {
|
|
1034
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
1035
|
-
if (response.id === requestId) {
|
|
1036
|
-
clearTimeout(timeout);
|
|
1037
|
-
client.destroy();
|
|
1038
|
-
if (response.type === 'result') {
|
|
1039
|
-
resolve(response.data.unscheduled);
|
|
1040
|
-
}
|
|
1041
|
-
else {
|
|
1042
|
-
reject(new Error(response.error || 'Unschedule failed'));
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
catch (e) {
|
|
1047
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
1048
|
-
}
|
|
1049
|
-
});
|
|
1050
|
-
client.on('error', (error) => {
|
|
1051
|
-
clearTimeout(timeout);
|
|
1052
|
-
client.destroy();
|
|
1053
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
1054
|
-
});
|
|
738
|
+
const result = await sendSimpleDaemonRequest({
|
|
739
|
+
type: 'unschedule',
|
|
740
|
+
photonName,
|
|
741
|
+
jobId,
|
|
1055
742
|
});
|
|
743
|
+
return result.unscheduled;
|
|
1056
744
|
}
|
|
1057
745
|
/**
|
|
1058
746
|
* List all scheduled jobs
|
|
1059
747
|
*/
|
|
1060
748
|
export async function listJobs(photonName) {
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const client = connectToDaemon(socketPath);
|
|
1065
|
-
const timeout = setTimeout(() => {
|
|
1066
|
-
client.destroy();
|
|
1067
|
-
reject(new Error('List jobs request timeout'));
|
|
1068
|
-
}, 5000);
|
|
1069
|
-
client.on('connect', () => {
|
|
1070
|
-
const request = {
|
|
1071
|
-
type: 'list_jobs',
|
|
1072
|
-
id: requestId,
|
|
1073
|
-
photonName,
|
|
1074
|
-
};
|
|
1075
|
-
client.write(JSON.stringify(request) + '\n');
|
|
1076
|
-
});
|
|
1077
|
-
client.on('data', (chunk) => {
|
|
1078
|
-
try {
|
|
1079
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
1080
|
-
if (response.id === requestId) {
|
|
1081
|
-
clearTimeout(timeout);
|
|
1082
|
-
client.destroy();
|
|
1083
|
-
if (response.type === 'result') {
|
|
1084
|
-
resolve(response.data.jobs || []);
|
|
1085
|
-
}
|
|
1086
|
-
else {
|
|
1087
|
-
reject(new Error(response.error || 'List jobs failed'));
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
catch (e) {
|
|
1092
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
1093
|
-
}
|
|
1094
|
-
});
|
|
1095
|
-
client.on('error', (error) => {
|
|
1096
|
-
clearTimeout(timeout);
|
|
1097
|
-
client.destroy();
|
|
1098
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
1099
|
-
});
|
|
749
|
+
const result = await sendSimpleDaemonRequest({
|
|
750
|
+
type: 'list_jobs',
|
|
751
|
+
photonName,
|
|
1100
752
|
});
|
|
753
|
+
return result.jobs || [];
|
|
1101
754
|
}
|
|
1102
755
|
/** Fire-and-forget helper for the new RPCs. Returns the parsed result.data. */
|
|
1103
756
|
async function sendSimpleDaemonRequest(req) {
|
|
757
|
+
const maxRetries = DEFAULT_DAEMON_COMMAND_RETRIES;
|
|
758
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
759
|
+
try {
|
|
760
|
+
await ensureDaemonReady();
|
|
761
|
+
return await sendSimpleDaemonRequestDirect(req);
|
|
762
|
+
}
|
|
763
|
+
catch (error) {
|
|
764
|
+
if (isDaemonConnectionError(error) && attempt < maxRetries) {
|
|
765
|
+
logger.info(`Daemon unreachable during ${req.type}, waiting for daemon...`);
|
|
766
|
+
await waitBeforeDaemonRetry(attempt);
|
|
767
|
+
await ensureDaemonReady();
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
throw new Error(`${req.type} failed`);
|
|
774
|
+
}
|
|
775
|
+
async function sendSimpleDaemonRequestDirect(req) {
|
|
1104
776
|
const socketPath = getGlobalSocketPath();
|
|
1105
777
|
const requestId = req.id || `${req.type}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1106
778
|
return new Promise((resolve, reject) => {
|
|
@@ -1195,9 +867,22 @@ export async function pingDaemon(photonName) {
|
|
|
1195
867
|
const requestId = `ping_${Date.now()}`;
|
|
1196
868
|
return new Promise((resolve) => {
|
|
1197
869
|
const client = tryConnectToDaemon(socketPath);
|
|
870
|
+
let done = false;
|
|
871
|
+
const finish = (alive) => {
|
|
872
|
+
if (done)
|
|
873
|
+
return;
|
|
874
|
+
done = true;
|
|
875
|
+
clearTimeout(timeout);
|
|
876
|
+
if (alive) {
|
|
877
|
+
client.end();
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
client.destroy();
|
|
881
|
+
}
|
|
882
|
+
resolve(alive);
|
|
883
|
+
};
|
|
1198
884
|
const timeout = setTimeout(() => {
|
|
1199
|
-
|
|
1200
|
-
resolve(false);
|
|
885
|
+
finish(false);
|
|
1201
886
|
}, 5000);
|
|
1202
887
|
client.on('connect', () => {
|
|
1203
888
|
const request = {
|
|
@@ -1211,9 +896,7 @@ export async function pingDaemon(photonName) {
|
|
|
1211
896
|
try {
|
|
1212
897
|
const response = JSON.parse(chunk.toString().trim());
|
|
1213
898
|
if (response.id === requestId && response.type === 'pong') {
|
|
1214
|
-
|
|
1215
|
-
client.destroy();
|
|
1216
|
-
resolve(true);
|
|
899
|
+
finish(true);
|
|
1217
900
|
}
|
|
1218
901
|
}
|
|
1219
902
|
catch (error) {
|
|
@@ -1221,14 +904,10 @@ export async function pingDaemon(photonName) {
|
|
|
1221
904
|
}
|
|
1222
905
|
});
|
|
1223
906
|
client.on('error', () => {
|
|
1224
|
-
|
|
1225
|
-
client.destroy();
|
|
1226
|
-
resolve(false);
|
|
907
|
+
finish(false);
|
|
1227
908
|
});
|
|
1228
909
|
client.on('end', () => {
|
|
1229
|
-
|
|
1230
|
-
client.destroy();
|
|
1231
|
-
resolve(false);
|
|
910
|
+
finish(false);
|
|
1232
911
|
});
|
|
1233
912
|
});
|
|
1234
913
|
}
|
|
@@ -1331,6 +1010,25 @@ export async function clearInstances(photonName, workingDir) {
|
|
|
1331
1010
|
});
|
|
1332
1011
|
}
|
|
1333
1012
|
export async function getEventsSince(photonName, channel, lastEventId) {
|
|
1013
|
+
const maxRetries = DEFAULT_DAEMON_COMMAND_RETRIES;
|
|
1014
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1015
|
+
try {
|
|
1016
|
+
await ensureDaemonReady();
|
|
1017
|
+
return await getEventsSinceDirect(photonName, channel, lastEventId);
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
if (isDaemonConnectionError(error) && attempt < maxRetries) {
|
|
1021
|
+
logger.info(`Daemon unreachable during get_events_since (${channel}), waiting for daemon...`);
|
|
1022
|
+
await waitBeforeDaemonRetry(attempt);
|
|
1023
|
+
await ensureDaemonReady();
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
throw error;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
throw new Error('get_events_since failed');
|
|
1030
|
+
}
|
|
1031
|
+
async function getEventsSinceDirect(photonName, channel, lastEventId) {
|
|
1334
1032
|
const socketPath = getGlobalSocketPath();
|
|
1335
1033
|
const requestId = `getevents_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1336
1034
|
return new Promise((resolve, reject) => {
|