🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

shell-cluster

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

shell-cluster - npm Package Compare versions

Comparing version
1.0.8
to
1.0.9
+66
scripts/test-pty-spawn.js
#!/usr/bin/env node
'use strict';
const http = require('http');
const { WebSocketServer, WebSocket } = require('ws');
const pty = require('node-pty');
const os = require('os');
const PORT = 19877;
// Test 1: Direct spawn
console.log('=== Test 1: Direct spawn ===');
try {
const p = pty.spawn('/bin/zsh', [], {
name: 'xterm-256color', cols: 80, rows: 24,
cwd: os.homedir(),
env: Object.assign({}, process.env, { TERM: 'xterm-256color' }),
});
console.log('OK: pid=' + p.pid);
p.kill();
} catch (e) {
console.log('FAIL:', e.message);
}
// Test 2: Spawn inside WS handler
console.log('\n=== Test 2: Spawn inside WebSocket handler ===');
const server = http.createServer();
const wss = new WebSocketServer({ noServer: true });
server.on('upgrade', (req, socket, head) => {
wss.handleUpgrade(req, socket, head, (ws) => {
console.log(' WS client connected');
try {
const p = pty.spawn('/bin/zsh', [], {
name: 'xterm-256color', cols: 80, rows: 24,
cwd: os.homedir(),
env: Object.assign({}, process.env, { TERM: 'xterm-256color' }),
});
console.log(' OK: pid=' + p.pid);
ws.send('OK:' + p.pid);
p.kill();
} catch (e) {
console.log(' FAIL:', e.message);
ws.send('FAIL:' + e.message);
}
ws.close();
});
});
server.listen(PORT, '127.0.0.1', () => {
console.log(' Server on port ' + PORT);
const ws = new WebSocket(`ws://127.0.0.1:${PORT}/raw?session=test&cols=80&rows=24`);
ws.on('message', (data) => {
console.log(' Client received: ' + data.toString());
});
ws.on('close', () => {
server.close(() => {
console.log('\nDone.');
process.exit(0);
});
});
ws.on('error', (e) => {
console.log(' Client error: ' + e.message);
server.close(() => process.exit(1));
});
});
#!/usr/bin/env node
'use strict';
const { WebSocket } = require('ws');
const sessionId = 'diag-' + Date.now();
const url = `ws://127.0.0.1:19876/raw?session=${sessionId}&cols=80&rows=24`;
console.log('Connecting to:', url);
const ws = new WebSocket(url);
ws.on('open', () => console.log('WS opened'));
ws.on('message', (data, isBinary) => {
if (isBinary) {
console.log('Binary data:', data.length, 'bytes');
} else {
const text = data.toString();
try {
const msg = JSON.parse(text);
console.log('JSON:', JSON.stringify(msg));
} catch {
console.log('Text:', text.slice(0, 200));
}
}
});
ws.on('close', (code, reason) => {
console.log(`Closed: code=${code} reason=${reason.toString()}`);
process.exit(0);
});
ws.on('error', (e) => {
console.log('Error:', e.message);
process.exit(1);
});
setTimeout(() => {
console.log('Timeout - closing');
ws.close();
}, 3000);
#!/usr/bin/env node
'use strict';
const { WebSocket } = require('ws');
// Test both localhost and 127.0.0.1
for (const host of ['localhost', '127.0.0.1']) {
const sessionId = 'diag-' + host + '-' + Date.now();
const url = `ws://${host}:19876/raw?session=${sessionId}&cols=80&rows=24`;
console.log(`\n--- Testing ${host} ---`);
console.log('URL:', url);
const ws = new WebSocket(url);
ws.on('open', () => console.log(` [${host}] opened`));
ws.on('message', (data, isBinary) => {
if (!isBinary) {
try {
const msg = JSON.parse(data.toString());
console.log(` [${host}] JSON:`, msg.type);
} catch {
console.log(` [${host}] text:`, data.toString().slice(0, 80));
}
} else {
console.log(` [${host}] binary: ${data.length} bytes`);
}
});
ws.on('close', (code, reason) => {
console.log(` [${host}] closed: code=${code} reason=${reason.toString()}`);
});
ws.on('error', (e) => {
console.log(` [${host}] ERROR: ${e.message}`);
});
setTimeout(() => ws.close(), 2000);
}
setTimeout(() => process.exit(0), 3000);
+1
-1
{
"name": "shell-cluster",
"version": "1.0.8",
"version": "1.0.9",
"description": "Decentralized remote shell access via tunnels — Node.js server with node-pty and xterm-headless",

@@ -5,0 +5,0 @@ "main": "src/index.js",

@@ -455,5 +455,8 @@ // --- State ---

ws.onclose = () => {
ws.onclose = (ev) => {
console.warn(`[WS] closed session=${sessionId} code=${ev.code} reason="${ev.reason}"`);
if (!attached) {
term.writeln('\r\n\x1b[2m[Disconnected]\x1b[0m');
term.writeln(`\r\n\x1b[2m[Disconnected: code=${ev.code}${ev.reason ? ' ' + ev.reason : ''}]\x1b[0m`);
} else {
term.writeln(`\r\n\x1b[2m[Disconnected: code=${ev.code}${ev.reason ? ' ' + ev.reason : ''}]\x1b[0m`);
}

@@ -464,3 +467,4 @@ sessionState._disconnected = true;

ws.onerror = () => {
ws.onerror = (ev) => {
console.error(`[WS] error session=${sessionId}`, ev);
term.writeln('\r\n\x1b[31m[Connection error]\x1b[0m');

@@ -467,0 +471,0 @@ };

@@ -209,3 +209,4 @@ /**

peerWs.on('close', () => {
peerWs.on('close', (code, reason) => {
console.log(`[DashboardServer] Peer WS closed code=${code} reason="${reason || ''}"`);
browserWs.close();

@@ -225,3 +226,4 @@ });

browserWs.on('close', () => {
browserWs.on('close', (code, reason) => {
console.log(`[DashboardServer] Browser WS closed code=${code} reason="${reason || ''}"`);
if (peerWs && peerWs.readyState === WebSocket.OPEN) {

@@ -228,0 +230,0 @@ peerWs.close();

@@ -71,9 +71,21 @@ /**

const ptyProcess = pty.spawn(shellCmd, [], {
name: 'xterm-256color',
cols,
rows,
cwd: os.homedir(),
env,
});
let ptyProcess;
try {
ptyProcess = pty.spawn(shellCmd, [], {
name: 'xterm-256color',
cols,
rows,
cwd: os.homedir(),
env,
});
} catch (e) {
const fs = require('fs');
const shellExists = fs.existsSync(shellCmd);
const homeExists = fs.existsSync(os.homedir());
console.log(`[ShellManager] ERROR: Failed to spawn shell '${shellCmd}'`);
console.log(`[ShellManager] shell exists: ${shellExists}, cwd exists: ${homeExists}, cols=${cols}, rows=${rows}`);
console.log(`[ShellManager] node-pty error: ${e.message}`);
if (e.stack) console.log(e.stack);
throw new Error(`Failed to spawn '${shellCmd}': ${e.message} (shell exists=${shellExists})`);
}

@@ -80,0 +92,0 @@ // Create headless terminal for state tracking

@@ -122,27 +122,57 @@ /**

// --- Batched output: accumulate PTY data, flush every 16ms (~60fps) ---
let outputBuf = [];
let flushTimer = null;
const flushOutput = () => {
flushTimer = null;
if (outputBuf.length === 0) return;
if (ws.readyState !== ws.OPEN) {
outputBuf = [];
return;
}
const combined = Buffer.concat(outputBuf);
outputBuf = [];
let str = combined.toString('utf-8');
str = stripTerminalQueries(str);
if (!str) return;
try {
ws.send(Buffer.from(str, 'utf-8'), (err) => {
if (err) {
console.warn(`[ShellServer] ws.send error session=${sessionId}: ${err.message}`);
return;
}
// Backpressure: pause PTY if WS buffer > 1MB
if (ws.bufferedAmount > 1024 * 1024) {
console.log(`[ShellServer] Backpressure ON session=${sessionId} buffered=${ws.bufferedAmount}`);
this._shellManager.pausePty(sessionId);
const check = () => {
if (ws.readyState !== ws.OPEN) {
// WS gone — resume PTY so it doesn't stay paused forever
this._shellManager.resumePty(sessionId);
return;
}
if (ws.bufferedAmount < 256 * 1024) {
console.log(`[ShellServer] Backpressure OFF session=${sessionId}`);
this._shellManager.resumePty(sessionId);
} else {
setTimeout(check, 50);
}
};
setTimeout(check, 50);
}
});
} catch (e) {
console.warn(`[ShellServer] ws.send threw session=${sessionId}: ${e.message}`);
}
};
const onOutput = (sid, data) => {
if (ws.readyState !== ws.OPEN) return;
let str = data.toString('utf-8');
str = stripTerminalQueries(str);
if (str) {
try {
ws.send(Buffer.from(str, 'utf-8'), (err) => {
if (err) return;
// Backpressure: pause PTY if WS buffer > 1MB
if (ws.bufferedAmount > 1024 * 1024) {
this._shellManager.pausePty(sessionId);
const check = () => {
if (ws.readyState !== ws.OPEN) return;
if (ws.bufferedAmount < 256 * 1024) {
this._shellManager.resumePty(sessionId);
} else {
setTimeout(check, 50);
}
};
setTimeout(check, 50);
}
});
} catch (e) {
// connection closed
}
outputBuf.push(data);
if (!flushTimer) {
flushTimer = setTimeout(flushOutput, 67);
}

@@ -196,3 +226,4 @@ };

} catch (e) {
console.error(`[ShellServer] Session setup failed:`, e.message);
console.log(`[ShellServer] ERROR: Session setup failed for session=${sessionId}: ${e.message}`);
if (e.stack) console.log(e.stack);
try {

@@ -240,9 +271,16 @@ ws.send(JSON.stringify({ type: 'error', error: e.message }));

ws.on('close', () => {
console.log(`[ShellServer] Raw client disconnected: session=${sessionId}`);
ws.on('close', (code, reason) => {
const reasonStr = reason ? reason.toString() : '';
console.log(`[ShellServer] WS closed session=${sessionId} code=${code} reason="${reasonStr}"`);
// Clean up flush timer
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
outputBuf = [];
this._shellManager.detach(sessionId, onOutput, onExit);
// Ensure PTY is resumed in case backpressure left it paused
this._shellManager.resumePty(sessionId);
});
ws.on('error', (err) => {
console.warn(`[ShellServer] WebSocket error for session=${sessionId}:`, err.message);
console.error(`[ShellServer] WS error session=${sessionId}: ${err.message}`);
if (err.code) console.error(`[ShellServer] code=${err.code}`);
});

@@ -249,0 +287,0 @@ }