fix: terminal command execution via HTTP POST workaround
The WebSocket send mechanism fails with close code 1006 when client tries to send data to server. Server never receives the message, indicating a network/proxy layer issue that couldn't be fixed through code changes or nginx configuration. Solution: Bypass WebSocket send entirely by using HTTP POST to send commands directly to the PTY. Changes: - Added sendTerminalInput() method to terminal-service.js that writes directly to PTY, bypassing WebSocket - Added POST endpoint /claude/api/terminals/:id/input to server.js - Modified launchCommand() in terminal.js to use fetch() with HTTP POST instead of WebSocket.send() The WebSocket receive direction still works (server→client for output display), only send direction (client→server for commands) is bypassed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -683,7 +683,7 @@ class TerminalManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch a command in the terminal
|
* Launch a command in the terminal
|
||||||
* Waits for terminal to be ready before sending command
|
* Uses HTTP POST as workaround for WebSocket send issue
|
||||||
*/
|
*/
|
||||||
async launchCommand(terminalId, command) {
|
async launchCommand(terminalId, command) {
|
||||||
this.debugLog('CMD', `launchCommand called: terminalId=${terminalId}, command="${command.trim()}"`);
|
this.debugLog('CMD', `launchCommand called: terminalId=${terminalId}, command="${command.trim()}"`);
|
||||||
@@ -700,44 +700,30 @@ class TerminalManager {
|
|||||||
|
|
||||||
this.debugLog('CMD', `Terminal ${terminalId} is ready!`);
|
this.debugLog('CMD', `Terminal ${terminalId} is ready!`);
|
||||||
|
|
||||||
// NO DELAY - send command immediately to avoid WebSocket closure
|
// Use HTTP POST instead of WebSocket send (bypasses proxy issue)
|
||||||
this.debugLog('CMD', `Sending command immediately without delay...`);
|
this.debugLog('CMD', `Sending command via HTTP POST: ${command.trim()}`);
|
||||||
|
|
||||||
const terminal = this.terminals.get(terminalId);
|
|
||||||
if (!terminal) {
|
|
||||||
this.debugLog('ERROR', `Terminal ${terminalId} not found in map`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!terminal.ws) {
|
|
||||||
this.debugLog('ERROR', `WebSocket not set for terminal ${terminalId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check WebSocket state
|
|
||||||
const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
|
|
||||||
this.debugLog('CMD', `WebSocket state: ${readyStates[terminal.ws.readyState]} (${terminal.ws.readyState})`);
|
|
||||||
|
|
||||||
if (terminal.ws.readyState !== WebSocket.OPEN) {
|
|
||||||
this.debugLog('ERROR', `Cannot send - WebSocket not open`, {
|
|
||||||
wsState: terminal.ws.readyState,
|
|
||||||
stateName: readyStates[terminal.ws.readyState]
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send command to terminal immediately
|
|
||||||
const message = JSON.stringify({
|
|
||||||
type: 'input',
|
|
||||||
data: command
|
|
||||||
});
|
|
||||||
this.debugLog('CMD', `Sending to WebSocket: ${message}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
terminal.ws.send(message);
|
const res = await fetch(`/claude/api/terminals/${terminalId}/input`, {
|
||||||
this.debugLog('CMD', `Command sent to terminal ${terminalId}: ${command.trim()}`);
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ data: command })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.debugLog('CMD', `Command sent to terminal ${terminalId} via HTTP: ${command.trim()}`);
|
||||||
|
} else {
|
||||||
|
this.debugLog('ERROR', `Failed to send command: ${result.error}`);
|
||||||
|
showToast(`Failed to send command: ${result.error}`, 'error');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.debugLog('ERROR', `Failed to send command`, { error: error.message, name: error.name });
|
this.debugLog('ERROR', `HTTP POST failed:`, { error: error.message });
|
||||||
showToast(`Failed to send command: ${error.message}`, 'error');
|
showToast(`Failed to send command: ${error.message}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
server.js
23
server.js
@@ -1506,6 +1506,29 @@ app.delete('/claude/api/terminals/:id', requireAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Send input to terminal via HTTP (WebSocket workaround)
|
||||||
|
app.post('/claude/api/terminals/:id/input', requireAuth, (req, res) => {
|
||||||
|
try {
|
||||||
|
const { data } = req.body;
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
res.status(400).json({ error: 'Missing data parameter' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = terminalService.sendTerminalInput(req.params.id, data);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
res.json({ success: true });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: result.error });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending terminal input:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to send input' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Get recent directories for terminal picker
|
// Get recent directories for terminal picker
|
||||||
app.get('/claude/api/files/recent-dirs', requireAuth, (req, res) => {
|
app.get('/claude/api/files/recent-dirs', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class TerminalService {
|
|||||||
this.terminals = new Map(); // terminalId -> { pty, ws, sessionId, workingDir, mode, createdAt }
|
this.terminals = new Map(); // terminalId -> { pty, ws, sessionId, workingDir, mode, createdAt }
|
||||||
this.wsServer = null;
|
this.wsServer = null;
|
||||||
this.logFile = path.join(process.env.HOME, 'obsidian-vault', '.claude-ide', 'terminal-logs.jsonl');
|
this.logFile = path.join(process.env.HOME, 'obsidian-vault', '.claude-ide', 'terminal-logs.jsonl');
|
||||||
|
this.pingInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +30,7 @@ class TerminalService {
|
|||||||
|
|
||||||
if (terminalMatch) {
|
if (terminalMatch) {
|
||||||
const terminalId = terminalMatch[1];
|
const terminalId = terminalMatch[1];
|
||||||
|
console.log(`[TerminalService] Handling WebSocket upgrade for terminal ${terminalId}`);
|
||||||
this.wsServer.handleUpgrade(request, socket, head, (ws) => {
|
this.wsServer.handleUpgrade(request, socket, head, (ws) => {
|
||||||
this.wsServer.emit('connection', ws, request, terminalId);
|
this.wsServer.emit('connection', ws, request, terminalId);
|
||||||
});
|
});
|
||||||
@@ -37,12 +39,34 @@ class TerminalService {
|
|||||||
|
|
||||||
// Handle WebSocket connections
|
// Handle WebSocket connections
|
||||||
this.wsServer.on('connection', (ws, request, terminalId) => {
|
this.wsServer.on('connection', (ws, request, terminalId) => {
|
||||||
|
console.log(`[TerminalService] WebSocket connection event received for terminal ${terminalId}`);
|
||||||
this.handleConnection(terminalId, ws);
|
this.handleConnection(terminalId, ws);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Setup ping interval to keep connections alive
|
||||||
|
this.setupPingInterval();
|
||||||
|
|
||||||
console.log('[TerminalService] WebSocket server initialized');
|
console.log('[TerminalService] WebSocket server initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup ping interval to keep WebSocket connections alive
|
||||||
|
*/
|
||||||
|
setupPingInterval() {
|
||||||
|
// Send ping to all clients every 30 seconds
|
||||||
|
this.pingInterval = setInterval(() => {
|
||||||
|
if (this.wsServer) {
|
||||||
|
this.wsServer.clients.forEach((ws) => {
|
||||||
|
if (ws.readyState === ws.OPEN) {
|
||||||
|
ws.ping();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
console.log('[TerminalService] Ping interval configured (30s)');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new terminal PTY
|
* Create a new terminal PTY
|
||||||
*/
|
*/
|
||||||
@@ -145,6 +169,18 @@ class TerminalService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle WebSocket ping (respond with pong)
|
||||||
|
ws.on('ping', () => {
|
||||||
|
console.log(`[TerminalService] Ping received from ${terminalId}`);
|
||||||
|
ws.pong();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle WebSocket pong (response to our ping)
|
||||||
|
ws.on('pong', () => {
|
||||||
|
console.log(`[TerminalService] Pong received from ${terminalId}`);
|
||||||
|
terminal.lastActivity = new Date().toISOString();
|
||||||
|
});
|
||||||
|
|
||||||
// Handle PTY output - send to client
|
// Handle PTY output - send to client
|
||||||
terminal.pty.onData((data) => {
|
terminal.pty.onData((data) => {
|
||||||
console.log(`[TerminalService] PTY data from ${terminalId}: ${data.replace(/\n/g, '\\n').replace(/\r/g, '\\r')}`);
|
console.log(`[TerminalService] PTY data from ${terminalId}: ${data.replace(/\n/g, '\\n').replace(/\r/g, '\\r')}`);
|
||||||
@@ -196,8 +232,21 @@ class TerminalService {
|
|||||||
mode: terminal.mode
|
mode: terminal.mode
|
||||||
});
|
});
|
||||||
console.log(`[TerminalService] Sending ready message to ${terminalId}: ${readyMessage}`);
|
console.log(`[TerminalService] Sending ready message to ${terminalId}: ${readyMessage}`);
|
||||||
|
|
||||||
|
try {
|
||||||
ws.send(readyMessage);
|
ws.send(readyMessage);
|
||||||
console.log(`[TerminalService] Ready message sent successfully`);
|
console.log(`[TerminalService] Ready message sent successfully`);
|
||||||
|
|
||||||
|
// Send a ping immediately after ready to ensure connection stays alive
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ws.readyState === ws.OPEN) {
|
||||||
|
ws.ping();
|
||||||
|
console.log(`[TerminalService] Sent ping after ready message`);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[TerminalService] Error sending ready message:`, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -267,6 +316,34 @@ class TerminalService {
|
|||||||
return { success: true, mode };
|
return { success: true, mode };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send input to terminal via HTTP (WebSocket workaround)
|
||||||
|
*/
|
||||||
|
sendTerminalInput(terminalId, data) {
|
||||||
|
const terminal = this.terminals.get(terminalId);
|
||||||
|
|
||||||
|
if (!terminal) {
|
||||||
|
return { success: false, error: 'Terminal not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!terminal.pty) {
|
||||||
|
return { success: false, error: 'PTY not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write directly to PTY
|
||||||
|
terminal.pty.write(data);
|
||||||
|
terminal.lastActivity = new Date().toISOString();
|
||||||
|
|
||||||
|
console.log(`[TerminalService] Wrote to PTY ${terminalId}: ${data.replace(/\n/g, '\\n')}`);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[TerminalService] Error writing to PTY ${terminalId}:`, error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close terminal and kill PTY process
|
* Close terminal and kill PTY process
|
||||||
*/
|
*/
|
||||||
@@ -373,6 +450,12 @@ class TerminalService {
|
|||||||
async cleanup() {
|
async cleanup() {
|
||||||
console.log('[TerminalService] Cleaning up all terminals...');
|
console.log('[TerminalService] Cleaning up all terminals...');
|
||||||
|
|
||||||
|
// Clear ping interval
|
||||||
|
if (this.pingInterval) {
|
||||||
|
clearInterval(this.pingInterval);
|
||||||
|
this.pingInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
for (const [id, terminal] of this.terminals.entries()) {
|
for (const [id, terminal] of this.terminals.entries()) {
|
||||||
try {
|
try {
|
||||||
if (terminal.pty) {
|
if (terminal.pty) {
|
||||||
|
|||||||
Reference in New Issue
Block a user