genbox 1.0.180 → 1.0.182
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.
|
@@ -508,6 +508,226 @@ function getDtachSocketDir() {
|
|
|
508
508
|
function getDtachSocketPath(sessionName) {
|
|
509
509
|
return path.join(getDtachSocketDir(), `${sessionName}.sock`);
|
|
510
510
|
}
|
|
511
|
+
/**
|
|
512
|
+
* Special key mappings (inspired by tmux's key-string.c)
|
|
513
|
+
* Maps key names to their terminal byte sequences
|
|
514
|
+
*/
|
|
515
|
+
const SPECIAL_KEYS = {
|
|
516
|
+
'Enter': '\r',
|
|
517
|
+
'Return': '\r',
|
|
518
|
+
'Tab': '\t',
|
|
519
|
+
'Escape': '\x1b',
|
|
520
|
+
'Space': ' ',
|
|
521
|
+
'Backspace': '\x7f',
|
|
522
|
+
'Delete': '\x1b[3~',
|
|
523
|
+
'Up': '\x1b[A',
|
|
524
|
+
'Down': '\x1b[B',
|
|
525
|
+
'Right': '\x1b[C',
|
|
526
|
+
'Left': '\x1b[D',
|
|
527
|
+
'Home': '\x1b[H',
|
|
528
|
+
'End': '\x1b[F',
|
|
529
|
+
'PageUp': '\x1b[5~',
|
|
530
|
+
'PageDown': '\x1b[6~',
|
|
531
|
+
'Insert': '\x1b[2~',
|
|
532
|
+
'F1': '\x1bOP',
|
|
533
|
+
'F2': '\x1bOQ',
|
|
534
|
+
'F3': '\x1bOR',
|
|
535
|
+
'F4': '\x1bOS',
|
|
536
|
+
'F5': '\x1b[15~',
|
|
537
|
+
'F6': '\x1b[17~',
|
|
538
|
+
'F7': '\x1b[18~',
|
|
539
|
+
'F8': '\x1b[19~',
|
|
540
|
+
'F9': '\x1b[20~',
|
|
541
|
+
'F10': '\x1b[21~',
|
|
542
|
+
'F11': '\x1b[23~',
|
|
543
|
+
'F12': '\x1b[24~',
|
|
544
|
+
};
|
|
545
|
+
/**
|
|
546
|
+
* Convert control key notation to byte (e.g., C-c -> \x03)
|
|
547
|
+
* Follows tmux convention: C-a through C-z map to 0x01-0x1a
|
|
548
|
+
*/
|
|
549
|
+
function parseControlKey(key) {
|
|
550
|
+
const match = key.match(/^[Cc]-([a-zA-Z@\[\]\\^_])$/);
|
|
551
|
+
if (!match)
|
|
552
|
+
return null;
|
|
553
|
+
const char = match[1].toLowerCase();
|
|
554
|
+
if (char >= 'a' && char <= 'z') {
|
|
555
|
+
return String.fromCharCode(char.charCodeAt(0) - 96); // a=1, z=26
|
|
556
|
+
}
|
|
557
|
+
// Special control chars
|
|
558
|
+
const specials = {
|
|
559
|
+
'@': 0, '[': 27, '\\': 28, ']': 29, '^': 30, '_': 31
|
|
560
|
+
};
|
|
561
|
+
if (specials[char] !== undefined) {
|
|
562
|
+
return String.fromCharCode(specials[char]);
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Send keys to a dtach session (inspired by tmux's send-keys)
|
|
568
|
+
*
|
|
569
|
+
* This function writes directly to dtach's stdin, bypassing shell interpretation.
|
|
570
|
+
* Like tmux, it supports:
|
|
571
|
+
* - Literal text
|
|
572
|
+
* - Special keys (Enter, Tab, Escape, etc.)
|
|
573
|
+
* - Control sequences (C-c, C-d, etc.)
|
|
574
|
+
*
|
|
575
|
+
* @param socketPath - Path to the dtach socket
|
|
576
|
+
* @param keys - Array of keys/text to send. Each element can be:
|
|
577
|
+
* - A string of literal text
|
|
578
|
+
* - A special key name like "Enter", "Tab", "Escape"
|
|
579
|
+
* - A control sequence like "C-c"
|
|
580
|
+
* @param options - Optional: addNewline (append Enter after all keys)
|
|
581
|
+
*/
|
|
582
|
+
function sendKeysToDtach(socketPath, keys, options = {}) {
|
|
583
|
+
// Build the byte sequence to send
|
|
584
|
+
let data = '';
|
|
585
|
+
for (const key of keys) {
|
|
586
|
+
// Check for special key
|
|
587
|
+
if (SPECIAL_KEYS[key]) {
|
|
588
|
+
data += SPECIAL_KEYS[key];
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
// Check for control key notation
|
|
592
|
+
const ctrlKey = parseControlKey(key);
|
|
593
|
+
if (ctrlKey) {
|
|
594
|
+
data += ctrlKey;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
// Literal text
|
|
598
|
+
data += key;
|
|
599
|
+
}
|
|
600
|
+
// Optionally add newline
|
|
601
|
+
if (options.addNewline) {
|
|
602
|
+
data += '\r';
|
|
603
|
+
}
|
|
604
|
+
// Write directly to dtach via spawn (bypasses shell interpretation)
|
|
605
|
+
// This is the key insight from tmux: write bytes directly, don't go through shell
|
|
606
|
+
const proc = (0, child_process_1.spawnSync)('dtach', ['-p', socketPath], {
|
|
607
|
+
input: data,
|
|
608
|
+
timeout: 5000,
|
|
609
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
610
|
+
});
|
|
611
|
+
if (proc.error) {
|
|
612
|
+
throw new Error(`Failed to send keys: ${proc.error.message}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Send keys to a remote dtach session via SSH
|
|
617
|
+
* Uses the same direct-write approach as local, but through SSH stdin forwarding
|
|
618
|
+
*/
|
|
619
|
+
function sendKeysToRemoteDtach(ipAddress, keyPath, socketPath, keys, options = {}) {
|
|
620
|
+
// Build the byte sequence
|
|
621
|
+
let data = '';
|
|
622
|
+
for (const key of keys) {
|
|
623
|
+
if (SPECIAL_KEYS[key]) {
|
|
624
|
+
data += SPECIAL_KEYS[key];
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const ctrlKey = parseControlKey(key);
|
|
628
|
+
if (ctrlKey) {
|
|
629
|
+
data += ctrlKey;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
data += key;
|
|
633
|
+
}
|
|
634
|
+
if (options.addNewline) {
|
|
635
|
+
data += '\r';
|
|
636
|
+
}
|
|
637
|
+
// SSH with stdin forwarding to dtach -p
|
|
638
|
+
const proc = (0, child_process_1.spawnSync)('ssh', [
|
|
639
|
+
'-i', keyPath,
|
|
640
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
641
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
642
|
+
'-o', 'ConnectTimeout=5',
|
|
643
|
+
`dev@${ipAddress}`,
|
|
644
|
+
`dtach -p ${socketPath}`
|
|
645
|
+
], {
|
|
646
|
+
input: data,
|
|
647
|
+
timeout: 30000,
|
|
648
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
649
|
+
});
|
|
650
|
+
if (proc.error) {
|
|
651
|
+
throw new Error(`Failed to send keys to remote: ${proc.error.message}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Legacy escape function for cases where shell command is unavoidable
|
|
656
|
+
* Handles special shell characters that could cause issues
|
|
657
|
+
*/
|
|
658
|
+
function escapeForShell(str) {
|
|
659
|
+
return str
|
|
660
|
+
.replace(/\\/g, '\\\\') // Backslashes first
|
|
661
|
+
.replace(/'/g, "'\\''") // Single quotes
|
|
662
|
+
.replace(/\$/g, '\\$') // Dollar signs
|
|
663
|
+
.replace(/`/g, '\\`') // Backticks
|
|
664
|
+
.replace(/!/g, '\\!') // History expansion
|
|
665
|
+
.replace(/"/g, '\\"'); // Double quotes
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Check if a dtach socket is actually alive (not just file exists)
|
|
669
|
+
* Attempts a brief connection to verify the process is running
|
|
670
|
+
*/
|
|
671
|
+
function isDtachSocketAlive(socketPath) {
|
|
672
|
+
if (!fs.existsSync(socketPath)) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
// Try to send an empty string to test if socket is alive
|
|
677
|
+
// Using timeout to avoid hanging on dead sockets
|
|
678
|
+
(0, child_process_1.execSync)(`echo -n "" | dtach -p "${socketPath}" 2>/dev/null`, {
|
|
679
|
+
timeout: 2000,
|
|
680
|
+
stdio: 'ignore'
|
|
681
|
+
});
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
// Socket exists but is stale - clean it up
|
|
686
|
+
try {
|
|
687
|
+
fs.unlinkSync(socketPath);
|
|
688
|
+
console.log(chalk_1.default.dim(`Cleaned up stale socket: ${path.basename(socketPath)}`));
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
// Ignore cleanup errors
|
|
692
|
+
}
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Check if a remote dtach socket is alive via SSH
|
|
698
|
+
*/
|
|
699
|
+
function isRemoteDtachSocketAlive(ipAddress, keyPath, socketPath) {
|
|
700
|
+
try {
|
|
701
|
+
(0, child_process_1.execSync)(`ssh -i "${keyPath}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 dev@${ipAddress} "echo -n '' | dtach -p ${socketPath}" 2>/dev/null`, { timeout: 5000, stdio: 'ignore' });
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Clean up all stale sockets in the local sockets directory
|
|
710
|
+
* Called on startup to ensure clean state
|
|
711
|
+
*/
|
|
712
|
+
function cleanupAllStaleSockets() {
|
|
713
|
+
const socketsDir = getDtachSocketDir();
|
|
714
|
+
let cleaned = 0;
|
|
715
|
+
try {
|
|
716
|
+
const files = fs.readdirSync(socketsDir);
|
|
717
|
+
for (const file of files) {
|
|
718
|
+
if (file.endsWith('.sock')) {
|
|
719
|
+
const socketPath = path.join(socketsDir, file);
|
|
720
|
+
if (!isDtachSocketAlive(socketPath)) {
|
|
721
|
+
cleaned++;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
// Directory might not exist yet
|
|
728
|
+
}
|
|
729
|
+
return cleaned;
|
|
730
|
+
}
|
|
511
731
|
/**
|
|
512
732
|
* Ensure local dtach is available or get user choice
|
|
513
733
|
* Returns: 'available' | 'skip' | 'cancel'
|
|
@@ -1603,12 +1823,12 @@ async function createNativeSession(options) {
|
|
|
1603
1823
|
});
|
|
1604
1824
|
}
|
|
1605
1825
|
/**
|
|
1606
|
-
* Check if a dtach session is running locally (
|
|
1826
|
+
* Check if a dtach session is running locally (verifies socket is actually alive)
|
|
1607
1827
|
*/
|
|
1608
1828
|
function isDtachSessionRunning(sessionName) {
|
|
1609
1829
|
try {
|
|
1610
1830
|
const socketPath = getDtachSocketPath(sessionName);
|
|
1611
|
-
return
|
|
1831
|
+
return isDtachSocketAlive(socketPath);
|
|
1612
1832
|
}
|
|
1613
1833
|
catch {
|
|
1614
1834
|
return false;
|
|
@@ -1970,6 +2190,11 @@ exports.sessionCommand = new commander_1.Command('session')
|
|
|
1970
2190
|
.addCommand(migrate_1.sessionMigrateCommand)
|
|
1971
2191
|
.action(async (sessionArg, providerArgs, options) => {
|
|
1972
2192
|
try {
|
|
2193
|
+
// Clean up stale sockets on startup (silently)
|
|
2194
|
+
const cleaned = cleanupAllStaleSockets();
|
|
2195
|
+
if (cleaned > 0) {
|
|
2196
|
+
console.log(chalk_1.default.dim(`Cleaned up ${cleaned} stale socket${cleaned > 1 ? 's' : ''}`));
|
|
2197
|
+
}
|
|
1973
2198
|
// Multi-session orchestration mode
|
|
1974
2199
|
if (options.multi) {
|
|
1975
2200
|
// If sessionArg looks like a provider spec, treat it as part of providers
|
|
@@ -233,20 +233,125 @@ class LocalOrchestrator {
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
/**
|
|
236
|
-
* Send
|
|
237
|
-
*
|
|
236
|
+
* Send keys directly to dtach stdin (inspired by tmux's send-keys)
|
|
237
|
+
* Bypasses shell interpretation by writing directly to the process
|
|
238
|
+
*/
|
|
239
|
+
sendKeysDirect(socketPath, data) {
|
|
240
|
+
const { spawnSync } = require('child_process');
|
|
241
|
+
const proc = spawnSync('dtach', ['-p', socketPath], {
|
|
242
|
+
input: data + '\r',
|
|
243
|
+
timeout: 5000,
|
|
244
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
245
|
+
});
|
|
246
|
+
if (proc.error) {
|
|
247
|
+
throw new Error(`Failed to send keys: ${proc.error.message}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Send keys to Docker container's dtach session
|
|
252
|
+
*/
|
|
253
|
+
sendKeysToDocker(containerId, socketPath, data) {
|
|
254
|
+
const { spawnSync } = require('child_process');
|
|
255
|
+
const proc = spawnSync('docker', [
|
|
256
|
+
'exec', '-i', containerId,
|
|
257
|
+
'dtach', '-p', socketPath
|
|
258
|
+
], {
|
|
259
|
+
input: data + '\r',
|
|
260
|
+
timeout: 10000,
|
|
261
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
262
|
+
});
|
|
263
|
+
if (proc.error) {
|
|
264
|
+
throw new Error(`Failed to send keys to docker: ${proc.error.message}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Send keys to Multipass VM's dtach session
|
|
269
|
+
*/
|
|
270
|
+
sendKeysToMultipass(vmName, socketPath, data) {
|
|
271
|
+
const { spawnSync } = require('child_process');
|
|
272
|
+
const proc = spawnSync('multipass', [
|
|
273
|
+
'exec', vmName, '--',
|
|
274
|
+
'dtach', '-p', socketPath
|
|
275
|
+
], {
|
|
276
|
+
input: data + '\r',
|
|
277
|
+
timeout: 10000,
|
|
278
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
279
|
+
});
|
|
280
|
+
if (proc.error) {
|
|
281
|
+
throw new Error(`Failed to send keys to multipass: ${proc.error.message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Send keys to remote dtach session via SSH stdin forwarding
|
|
286
|
+
*/
|
|
287
|
+
sendKeysToRemote(ipAddress, keyPath, socketPath, data) {
|
|
288
|
+
const { spawnSync } = require('child_process');
|
|
289
|
+
const proc = spawnSync('ssh', [
|
|
290
|
+
'-i', keyPath,
|
|
291
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
292
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
293
|
+
'-o', 'ConnectTimeout=5',
|
|
294
|
+
`dev@${ipAddress}`,
|
|
295
|
+
`dtach -p ${socketPath}`
|
|
296
|
+
], {
|
|
297
|
+
input: data + '\r',
|
|
298
|
+
timeout: 30000,
|
|
299
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
300
|
+
});
|
|
301
|
+
if (proc.error) {
|
|
302
|
+
throw new Error(`Failed to send keys to remote: ${proc.error.message}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Send a prompt to a local session via dtach -p (push mode)
|
|
307
|
+
* Uses direct stdin write (inspired by tmux's send-keys) to bypass shell interpretation
|
|
238
308
|
*/
|
|
239
309
|
async sendPromptToLocalSession(session, prompt) {
|
|
240
|
-
|
|
241
|
-
|
|
310
|
+
const os = require('os');
|
|
311
|
+
const path = require('path');
|
|
312
|
+
try {
|
|
313
|
+
switch (session.isolation) {
|
|
314
|
+
case 'docker':
|
|
315
|
+
if (!session.containerId)
|
|
316
|
+
return;
|
|
317
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
318
|
+
this.sendKeysToDocker(session.containerId, `/home/dev/.genbox/sockets/${session.name}.sock`, prompt);
|
|
319
|
+
break;
|
|
320
|
+
case 'multipass':
|
|
321
|
+
if (!session.vmName)
|
|
322
|
+
return;
|
|
323
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
324
|
+
this.sendKeysToMultipass(session.vmName, `/home/ubuntu/.genbox/sockets/${session.name}.sock`, prompt);
|
|
325
|
+
break;
|
|
326
|
+
case 'native':
|
|
327
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
328
|
+
const socketPath = path.join(os.homedir(), '.genbox', 'sockets', `${session.name}.sock`);
|
|
329
|
+
this.sendKeysDirect(socketPath, prompt);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
console.log(chalk_1.default.yellow(`Warning: Could not send prompt to ${session.name}: ${error.message}`));
|
|
335
|
+
}
|
|
242
336
|
}
|
|
243
337
|
/**
|
|
244
|
-
* Send a prompt to a cloud session
|
|
245
|
-
*
|
|
338
|
+
* Send a prompt to a cloud session via SSH + dtach -p
|
|
339
|
+
* Uses direct stdin forwarding to bypass shell interpretation
|
|
246
340
|
*/
|
|
247
341
|
async sendPromptToCloudSession(session, prompt) {
|
|
248
|
-
|
|
249
|
-
|
|
342
|
+
if (!session.ipAddress) {
|
|
343
|
+
console.log(chalk_1.default.yellow(`Warning: Cloud session ${session.name} has no IP address`));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const socketPath = `/home/dev/.genbox/sockets/${session.name}.sock`;
|
|
347
|
+
try {
|
|
348
|
+
const keyPath = this.getPrivateSshKey();
|
|
349
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
350
|
+
this.sendKeysToRemote(session.ipAddress, keyPath, socketPath, prompt);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
console.log(chalk_1.default.yellow(`Warning: Could not send prompt to cloud session ${session.name}: ${error.message}`));
|
|
354
|
+
}
|
|
250
355
|
}
|
|
251
356
|
/**
|
|
252
357
|
* Get SSH key path for cloud sessions
|
|
@@ -293,7 +293,7 @@ class LocalSessionManager {
|
|
|
293
293
|
checkNativeStatus(session) {
|
|
294
294
|
try {
|
|
295
295
|
const socketPath = path.join(os.homedir(), '.genbox', 'sockets', `${session.name}.sock`);
|
|
296
|
-
if (
|
|
296
|
+
if (this.isDtachSocketAlive(socketPath)) {
|
|
297
297
|
return 'running';
|
|
298
298
|
}
|
|
299
299
|
return 'stopped';
|
|
@@ -302,6 +302,80 @@ class LocalSessionManager {
|
|
|
302
302
|
return 'stopped';
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Check if a dtach socket is actually alive (not just file exists)
|
|
307
|
+
*/
|
|
308
|
+
isDtachSocketAlive(socketPath) {
|
|
309
|
+
if (!fs.existsSync(socketPath)) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
// Try to send an empty string to test if socket is alive
|
|
314
|
+
(0, child_process_1.execSync)(`echo -n "" | dtach -p "${socketPath}" 2>/dev/null`, {
|
|
315
|
+
timeout: 2000,
|
|
316
|
+
stdio: 'ignore'
|
|
317
|
+
});
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Socket exists but is stale - clean it up
|
|
322
|
+
try {
|
|
323
|
+
fs.unlinkSync(socketPath);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// Ignore cleanup errors
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Send keys directly to dtach stdin (inspired by tmux's send-keys)
|
|
333
|
+
* Bypasses shell interpretation by writing directly to the process
|
|
334
|
+
*/
|
|
335
|
+
sendKeysDirect(socketPath, data) {
|
|
336
|
+
const proc = (0, child_process_1.spawnSync)('dtach', ['-p', socketPath], {
|
|
337
|
+
input: data + '\r', // Add Enter key
|
|
338
|
+
timeout: 5000,
|
|
339
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
340
|
+
});
|
|
341
|
+
if (proc.error) {
|
|
342
|
+
throw new Error(`Failed to send keys: ${proc.error.message}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Send keys to Docker container's dtach session
|
|
347
|
+
* Uses docker exec with stdin forwarding
|
|
348
|
+
*/
|
|
349
|
+
sendKeysToDocker(containerId, socketPath, data) {
|
|
350
|
+
const proc = (0, child_process_1.spawnSync)('docker', [
|
|
351
|
+
'exec', '-i', containerId,
|
|
352
|
+
'dtach', '-p', socketPath
|
|
353
|
+
], {
|
|
354
|
+
input: data + '\r',
|
|
355
|
+
timeout: 10000,
|
|
356
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
357
|
+
});
|
|
358
|
+
if (proc.error) {
|
|
359
|
+
throw new Error(`Failed to send keys to docker: ${proc.error.message}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Send keys to Multipass VM's dtach session
|
|
364
|
+
* Uses multipass exec with stdin forwarding
|
|
365
|
+
*/
|
|
366
|
+
sendKeysToMultipass(vmName, socketPath, data) {
|
|
367
|
+
const proc = (0, child_process_1.spawnSync)('multipass', [
|
|
368
|
+
'exec', vmName, '--',
|
|
369
|
+
'dtach', '-p', socketPath
|
|
370
|
+
], {
|
|
371
|
+
input: data + '\r',
|
|
372
|
+
timeout: 10000,
|
|
373
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
374
|
+
});
|
|
375
|
+
if (proc.error) {
|
|
376
|
+
throw new Error(`Failed to send keys to multipass: ${proc.error.message}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
305
379
|
/**
|
|
306
380
|
* List all local sessions
|
|
307
381
|
*/
|
|
@@ -866,13 +940,26 @@ final_message: "Cloud-init completed successfully"
|
|
|
866
940
|
this.unregisterSession(sessionId);
|
|
867
941
|
}
|
|
868
942
|
/**
|
|
869
|
-
* Send a prompt to a session
|
|
870
|
-
*
|
|
871
|
-
* This method is kept for API compatibility but may not work as expected.
|
|
872
|
-
* @deprecated dtach doesn't support programmatic prompt sending
|
|
943
|
+
* Send a prompt to a session via dtach -p (push mode)
|
|
944
|
+
* Uses direct stdin write (inspired by tmux's send-keys) to bypass shell interpretation
|
|
873
945
|
*/
|
|
874
946
|
async sendPromptToSession(session, prompt) {
|
|
875
|
-
|
|
947
|
+
switch (session.isolation) {
|
|
948
|
+
case 'docker':
|
|
949
|
+
if (!session.containerId)
|
|
950
|
+
throw new Error('Session has no container ID');
|
|
951
|
+
this.sendKeysToDocker(session.containerId, `/home/dev/.genbox/sockets/${session.name}.sock`, prompt);
|
|
952
|
+
break;
|
|
953
|
+
case 'multipass':
|
|
954
|
+
if (!session.vmName)
|
|
955
|
+
throw new Error('Session has no VM name');
|
|
956
|
+
this.sendKeysToMultipass(session.vmName, `/home/ubuntu/.genbox/sockets/${session.name}.sock`, prompt);
|
|
957
|
+
break;
|
|
958
|
+
case 'native':
|
|
959
|
+
const socketPath = path.join(os.homedir(), '.genbox', 'sockets', `${session.name}.sock`);
|
|
960
|
+
this.sendKeysDirect(socketPath, prompt);
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
876
963
|
// Update last activity
|
|
877
964
|
session.lastActivityAt = new Date();
|
|
878
965
|
this.updateSession(session);
|
|
@@ -233,17 +233,44 @@ class NativeSessionManager {
|
|
|
233
233
|
return this.listSessions().filter(s => this.isSessionRunning(s.dtachSocketName || s.name));
|
|
234
234
|
}
|
|
235
235
|
/**
|
|
236
|
-
* Check if a dtach session is running (
|
|
236
|
+
* Check if a dtach session is running (verifies socket is actually alive)
|
|
237
237
|
*/
|
|
238
238
|
isSessionRunning(sessionName) {
|
|
239
239
|
try {
|
|
240
240
|
const socketPath = path.join(os.homedir(), '.genbox', 'sockets', `${sessionName}.sock`);
|
|
241
|
-
return
|
|
241
|
+
return this.isDtachSocketAlive(socketPath);
|
|
242
242
|
}
|
|
243
243
|
catch {
|
|
244
244
|
return false;
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Check if a dtach socket is actually alive (not just file exists)
|
|
249
|
+
*/
|
|
250
|
+
isDtachSocketAlive(socketPath) {
|
|
251
|
+
if (!fs.existsSync(socketPath)) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
// Try to send an empty string to test if socket is alive
|
|
256
|
+
const { execSync } = require('child_process');
|
|
257
|
+
execSync(`echo -n "" | dtach -p "${socketPath}" 2>/dev/null`, {
|
|
258
|
+
timeout: 2000,
|
|
259
|
+
stdio: 'ignore'
|
|
260
|
+
});
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Socket exists but is stale - clean it up
|
|
265
|
+
try {
|
|
266
|
+
fs.unlinkSync(socketPath);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Ignore cleanup errors
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
247
274
|
/**
|
|
248
275
|
* Update session status
|
|
249
276
|
*/
|