genbox 1.0.181 → 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
|
|
@@ -13,7 +13,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
13
13
|
exports.LocalOrchestrator = void 0;
|
|
14
14
|
exports.parseProviderSpec = parseProviderSpec;
|
|
15
15
|
exports.getLocalOrchestrator = getLocalOrchestrator;
|
|
16
|
-
const child_process_1 = require("child_process");
|
|
17
16
|
const chalk_1 = __importDefault(require("chalk"));
|
|
18
17
|
const local_session_manager_1 = require("./local-session-manager");
|
|
19
18
|
/**
|
|
@@ -233,11 +232,81 @@ class LocalOrchestrator {
|
|
|
233
232
|
await this.sendPromptToCloudSession(orchSession.session, prompt);
|
|
234
233
|
}
|
|
235
234
|
}
|
|
235
|
+
/**
|
|
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
|
+
}
|
|
236
305
|
/**
|
|
237
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
|
-
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
241
310
|
const os = require('os');
|
|
242
311
|
const path = require('path');
|
|
243
312
|
try {
|
|
@@ -246,18 +315,18 @@ class LocalOrchestrator {
|
|
|
246
315
|
if (!session.containerId)
|
|
247
316
|
return;
|
|
248
317
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
249
|
-
|
|
318
|
+
this.sendKeysToDocker(session.containerId, `/home/dev/.genbox/sockets/${session.name}.sock`, prompt);
|
|
250
319
|
break;
|
|
251
320
|
case 'multipass':
|
|
252
321
|
if (!session.vmName)
|
|
253
322
|
return;
|
|
254
323
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
255
|
-
|
|
324
|
+
this.sendKeysToMultipass(session.vmName, `/home/ubuntu/.genbox/sockets/${session.name}.sock`, prompt);
|
|
256
325
|
break;
|
|
257
326
|
case 'native':
|
|
258
327
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
259
328
|
const socketPath = path.join(os.homedir(), '.genbox', 'sockets', `${session.name}.sock`);
|
|
260
|
-
|
|
329
|
+
this.sendKeysDirect(socketPath, prompt);
|
|
261
330
|
break;
|
|
262
331
|
}
|
|
263
332
|
}
|
|
@@ -267,18 +336,18 @@ class LocalOrchestrator {
|
|
|
267
336
|
}
|
|
268
337
|
/**
|
|
269
338
|
* Send a prompt to a cloud session via SSH + dtach -p
|
|
339
|
+
* Uses direct stdin forwarding to bypass shell interpretation
|
|
270
340
|
*/
|
|
271
341
|
async sendPromptToCloudSession(session, prompt) {
|
|
272
342
|
if (!session.ipAddress) {
|
|
273
343
|
console.log(chalk_1.default.yellow(`Warning: Cloud session ${session.name} has no IP address`));
|
|
274
344
|
return;
|
|
275
345
|
}
|
|
276
|
-
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
277
346
|
const socketPath = `/home/dev/.genbox/sockets/${session.name}.sock`;
|
|
278
347
|
try {
|
|
279
348
|
const keyPath = this.getPrivateSshKey();
|
|
280
349
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
281
|
-
|
|
350
|
+
this.sendKeysToRemote(session.ipAddress, keyPath, socketPath, prompt);
|
|
282
351
|
}
|
|
283
352
|
catch (error) {
|
|
284
353
|
console.log(chalk_1.default.yellow(`Warning: Could not send prompt to cloud session ${session.name}: ${error.message}`));
|
|
@@ -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
|
*/
|
|
@@ -867,25 +941,23 @@ final_message: "Cloud-init completed successfully"
|
|
|
867
941
|
}
|
|
868
942
|
/**
|
|
869
943
|
* Send a prompt to a session via dtach -p (push mode)
|
|
870
|
-
*
|
|
944
|
+
* Uses direct stdin write (inspired by tmux's send-keys) to bypass shell interpretation
|
|
871
945
|
*/
|
|
872
946
|
async sendPromptToSession(session, prompt) {
|
|
873
|
-
// Escape the prompt for shell and add Enter key
|
|
874
|
-
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
875
947
|
switch (session.isolation) {
|
|
876
948
|
case 'docker':
|
|
877
949
|
if (!session.containerId)
|
|
878
950
|
throw new Error('Session has no container ID');
|
|
879
|
-
|
|
951
|
+
this.sendKeysToDocker(session.containerId, `/home/dev/.genbox/sockets/${session.name}.sock`, prompt);
|
|
880
952
|
break;
|
|
881
953
|
case 'multipass':
|
|
882
954
|
if (!session.vmName)
|
|
883
955
|
throw new Error('Session has no VM name');
|
|
884
|
-
|
|
956
|
+
this.sendKeysToMultipass(session.vmName, `/home/ubuntu/.genbox/sockets/${session.name}.sock`, prompt);
|
|
885
957
|
break;
|
|
886
958
|
case 'native':
|
|
887
959
|
const socketPath = path.join(os.homedir(), '.genbox', 'sockets', `${session.name}.sock`);
|
|
888
|
-
|
|
960
|
+
this.sendKeysDirect(socketPath, prompt);
|
|
889
961
|
break;
|
|
890
962
|
}
|
|
891
963
|
// Update last activity
|
|
@@ -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
|
*/
|