Ditzzy AF commited on
Commit
d74a91d
Β·
1 Parent(s): 2a7c094

feat(app.js): Update default port, add platform detection, and enhance system monitoring

Browse files

- Changed default port from 7860 to 3000.
- Added platform detection for Windows, Linux, and macOS.
- Enhanced `SystemMonitor` class to include CPU and network history, and improved memory and disk usage calculations.
- Updated middleware configuration for `helmet` to include more secure content security policies.

Files changed (1) hide show
  1. app.js +403 -75
app.js CHANGED
@@ -26,15 +26,20 @@ const io = new IOServer(httpServer, {
26
  }
27
  });
28
 
29
- const PORT = process.env.PORT || 7860;
30
  const SESSION_SECRET = process.env.SESSION_SECRET || "devsecret";
31
  const PASSWORD_HASH = process.env.WEBTERM_PASSWORD_HASH;
32
 
 
33
  const isWindows = process.platform === "win32";
 
 
34
 
35
  // === System Utilities ===
36
  class SystemMonitor {
37
  constructor() {
 
 
38
  this.previousNetworkStats = null;
39
  }
40
 
@@ -54,6 +59,7 @@ class SystemMonitor {
54
  cpuAverage() {
55
  const cpus = os.cpus();
56
  let user = 0, nice = 0, sys = 0, idle = 0, irq = 0;
 
57
  for (let cpu of cpus) {
58
  user += cpu.times.user;
59
  nice += cpu.times.nice || 0;
@@ -61,6 +67,7 @@ class SystemMonitor {
61
  irq += cpu.times.irq || 0;
62
  idle += cpu.times.idle;
63
  }
 
64
  return {
65
  idle: idle / cpus.length,
66
  total: (user + nice + sys + idle + irq) / cpus.length
@@ -71,10 +78,11 @@ class SystemMonitor {
71
  const totalMem = os.totalmem();
72
  const freeMem = os.freemem();
73
  const usedMem = totalMem - freeMem;
 
74
  return {
75
- total: Math.round(totalMem / 1024 / 1024),
76
- used: Math.round(usedMem / 1024 / 1024),
77
- free: Math.round(freeMem / 1024 / 1024),
78
  percentage: Math.round((usedMem / totalMem) * 100)
79
  };
80
  }
@@ -84,23 +92,28 @@ class SystemMonitor {
84
  if (isWindows) {
85
  const { stdout } = await execAsync('wmic logicaldisk get size,freespace,caption /format:csv');
86
  const lines = stdout.split('\n').filter(line => line.includes(':'));
 
87
  if (lines.length > 0) {
88
  const parts = lines[0].split(',');
89
  if (parts.length >= 3) {
90
  const freeSpace = parseInt(parts[1]) || 0;
91
  const totalSpace = parseInt(parts[2]) || 1;
92
  const usedSpace = totalSpace - freeSpace;
 
 
93
  return {
94
- total: Math.round(totalSpace / 1024 / 1024 / 1024),
95
- used: Math.round(usedSpace / 1024 / 1024 / 1024),
96
- free: Math.round(freeSpace / 1024 / 1024 / 1024),
97
- percentage: Math.round((usedSpace / totalSpace) * 100)
98
  };
99
  }
100
  }
101
  } else {
 
102
  const { stdout } = await execAsync('df -h / | tail -1');
103
  const parts = stdout.trim().split(/\s+/);
 
104
  if (parts.length >= 5) {
105
  const percentage = parseInt(parts[4].replace('%', '')) || 0;
106
  return {
@@ -111,66 +124,208 @@ class SystemMonitor {
111
  };
112
  }
113
  }
114
- } catch {
115
- return { total: 0, used: 0, free: 0, percentage: 0 };
116
  }
 
117
  return { total: 0, used: 0, free: 0, percentage: 0 };
118
  }
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  formatUptime(seconds) {
121
  const days = Math.floor(seconds / 86400);
122
  const hours = Math.floor((seconds % 86400) / 3600);
123
  const minutes = Math.floor((seconds % 3600) / 60);
 
124
  if (days > 0) return `${days}d ${hours}h ${minutes}m`;
125
  if (hours > 0) return `${hours}h ${minutes}m`;
126
  return `${minutes}m`;
127
  }
128
 
129
  async getSystemInfo() {
130
- const [cpuUsage, memInfo, diskInfo] = await Promise.all([
131
- this.getCPUUsage(),
132
- this.getMemoryInfo(),
133
- this.getDiskUsage()
134
- ]);
135
- const cpuInfo = os.cpus()[0];
136
- return {
137
- platform: os.platform(),
138
- release: os.release(),
139
- arch: os.arch(),
140
- hostname: os.hostname(),
141
- uptime: this.formatUptime(os.uptime()),
142
- cpu_model: cpuInfo.model,
143
- cpu_cores: os.cpus().length,
144
- cpu_usage: cpuUsage,
145
- total_mem: memInfo.total,
146
- used_mem: memInfo.used,
147
- free_mem: memInfo.free,
148
- mem_usage: memInfo.percentage,
149
- disk_total: diskInfo.total,
150
- disk_used: diskInfo.used,
151
- disk_free: diskInfo.free,
152
- disk_usage: diskInfo.percentage,
153
- timestamp: Date.now()
154
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  }
156
  }
157
 
158
  const systemMonitor = new SystemMonitor();
159
 
160
  // === Middleware ===
161
- app.use(helmet());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  app.use(express.urlencoded({ extended: false }));
163
  app.use(express.json());
164
  app.use(express.static(path.join(__dirname, "public")));
165
  app.set("view engine", "ejs");
166
  app.set("views", path.join(__dirname, "views"));
167
 
 
168
  app.use(
169
  session({
170
  secret: SESSION_SECRET,
171
  resave: false,
172
  saveUninitialized: false,
173
- cookie: { httpOnly: true, sameSite: "lax", secure: false, maxAge: 24*60*60*1000 }
 
 
 
 
 
174
  })
175
  );
176
 
@@ -181,17 +336,29 @@ function requireAuth(req, res, next) {
181
  }
182
 
183
  // === Routes ===
184
- app.get("/login", (req, res) => res.render("login", { error: null }));
 
 
 
185
 
186
  app.post("/login", async (req, res) => {
187
  const { password } = req.body;
188
- if (!PASSWORD_HASH) return res.render("login", { error: "Server not configured" });
189
- const valid = await bcrypt.compare(password || "", PASSWORD_HASH);
190
- if (valid) {
191
- req.session.authed = true;
192
- return res.redirect("/");
193
- } else {
194
- return res.render("login", { error: "Invalid password" });
 
 
 
 
 
 
 
 
 
195
  }
196
  });
197
 
@@ -199,18 +366,25 @@ app.get("/logout", (req, res) => {
199
  req.session.destroy(() => res.redirect("/login"));
200
  });
201
 
202
- app.get("/", requireAuth, (req, res) => res.render("terminal"));
 
 
203
 
204
  app.get("/sysinfo", requireAuth, async (req, res) => {
205
  try {
206
- const info = await systemMonitor.getSystemInfo();
207
- res.json(info);
208
- } catch {
209
- res.status(500).json({ error: "Failed to fetch system info" });
 
 
 
 
 
210
  }
211
  });
212
 
213
- // === Terminal ===
214
  class Terminal {
215
  constructor(socket) {
216
  this.socket = socket;
@@ -220,75 +394,229 @@ class Terminal {
220
 
221
  async initialize() {
222
  try {
 
223
  const nodePty = await import("node-pty");
224
  return this.initWithPty(nodePty);
225
- } catch {
 
226
  return this.initWithSpawn();
227
  }
228
  }
229
 
230
  initWithPty(nodePty) {
231
  const shell = this.getShell();
232
- const env = { ...process.env, TERM: isWindows ? 'windows-ansi' : 'xterm-256color', COLORTERM: 'truecolor' };
 
 
 
 
 
 
 
 
 
233
  this.process = nodePty.spawn(shell, [], {
234
  name: isWindows ? 'windows-ansi' : 'xterm-256color',
235
  cols: 80,
236
  rows: 24,
237
- cwd: '/app', // PAKSA writable directory
238
  env: env,
239
- useConpty: isWindows
240
  });
241
 
242
- this.process.onData(data => this.isActive && this.socket.emit("terminal_output", data));
243
- this.process.onExit(() => this.isActive && this.socket.emit("terminal_output", "\r\n\x1b[91mTerminal session ended\x1b[0m\r\n"));
 
 
 
244
 
245
- this.socket.on("terminal_input", data => this.process.write(data));
246
- this.socket.on("terminal_resize", ({ cols, rows }) => this.process.resize(cols || 80, rows || 24));
 
 
 
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  return true;
249
  }
250
 
251
  initWithSpawn() {
252
  const shell = this.getShell();
253
- const args = isWindows ? [] : ["-l"];
254
- this.process = spawn(shell, args, { cwd: '/app', env: process.env, stdio: ['pipe', 'pipe', 'pipe'] });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
- this.process.stdout.on("data", data => this.isActive && this.socket.emit("terminal_output", data.toString()));
257
- this.process.stderr.on("data", data => this.isActive && this.socket.emit("terminal_output", data.toString()));
258
- this.process.on("exit", code => this.isActive && this.socket.emit("terminal_output", `\r\n\x1b[91mProcess exited with code ${code}\x1b[0m\r\n`));
 
 
 
 
 
 
 
 
259
 
260
- this.socket.on("terminal_input", data => this.process.stdin.write(data));
261
  return true;
262
  }
263
 
264
  getShell() {
265
- return isWindows ? process.env.COMSPEC || "cmd.exe" : process.env.SHELL || "/bin/bash";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  }
267
 
268
  destroy() {
269
  this.isActive = false;
270
  if (this.process) {
271
- try { this.process.kill?.(); } catch {}
 
 
 
 
 
 
 
 
272
  }
273
  }
274
  }
275
 
276
- // === Socket.IO ===
277
  io.on("connection", async (socket) => {
 
 
 
278
  const terminal = new Terminal(socket);
279
- await terminal.initialize();
280
 
281
- const systemInterval = setInterval(async () => {
282
- if (socket.connected) {
283
- const info = await systemMonitor.getSystemInfo();
284
- socket.emit("system_info", info);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  }
286
  }, 2000);
287
 
288
- socket.on("disconnect", () => { clearInterval(systemInterval); terminal.destroy(); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  });
290
 
291
  // === Start Server ===
292
  httpServer.listen(PORT, () => {
293
- console.log(`βœ… Web Terminal running on port ${PORT}`);
 
 
 
294
  });
 
 
 
26
  }
27
  });
28
 
29
+ const PORT = process.env.PORT || 3000;
30
  const SESSION_SECRET = process.env.SESSION_SECRET || "devsecret";
31
  const PASSWORD_HASH = process.env.WEBTERM_PASSWORD_HASH;
32
 
33
+ // Platform detection
34
  const isWindows = process.platform === "win32";
35
+ const isLinux = process.platform === "linux";
36
+ const isMacOS = process.platform === "darwin";
37
 
38
  // === System Utilities ===
39
  class SystemMonitor {
40
  constructor() {
41
+ this.cpuHistory = [];
42
+ this.networkHistory = [];
43
  this.previousNetworkStats = null;
44
  }
45
 
 
59
  cpuAverage() {
60
  const cpus = os.cpus();
61
  let user = 0, nice = 0, sys = 0, idle = 0, irq = 0;
62
+
63
  for (let cpu of cpus) {
64
  user += cpu.times.user;
65
  nice += cpu.times.nice || 0;
 
67
  irq += cpu.times.irq || 0;
68
  idle += cpu.times.idle;
69
  }
70
+
71
  return {
72
  idle: idle / cpus.length,
73
  total: (user + nice + sys + idle + irq) / cpus.length
 
78
  const totalMem = os.totalmem();
79
  const freeMem = os.freemem();
80
  const usedMem = totalMem - freeMem;
81
+
82
  return {
83
+ total: Math.round(totalMem / 1024 / 1024), // MB
84
+ used: Math.round(usedMem / 1024 / 1024), // MB
85
+ free: Math.round(freeMem / 1024 / 1024), // MB
86
  percentage: Math.round((usedMem / totalMem) * 100)
87
  };
88
  }
 
92
  if (isWindows) {
93
  const { stdout } = await execAsync('wmic logicaldisk get size,freespace,caption /format:csv');
94
  const lines = stdout.split('\n').filter(line => line.includes(':'));
95
+
96
  if (lines.length > 0) {
97
  const parts = lines[0].split(',');
98
  if (parts.length >= 3) {
99
  const freeSpace = parseInt(parts[1]) || 0;
100
  const totalSpace = parseInt(parts[2]) || 1;
101
  const usedSpace = totalSpace - freeSpace;
102
+ const percentage = Math.round((usedSpace / totalSpace) * 100);
103
+
104
  return {
105
+ total: Math.round(totalSpace / 1024 / 1024 / 1024), // GB
106
+ used: Math.round(usedSpace / 1024 / 1024 / 1024), // GB
107
+ free: Math.round(freeSpace / 1024 / 1024 / 1024), // GB
108
+ percentage: percentage
109
  };
110
  }
111
  }
112
  } else {
113
+ // Linux/macOS
114
  const { stdout } = await execAsync('df -h / | tail -1');
115
  const parts = stdout.trim().split(/\s+/);
116
+
117
  if (parts.length >= 5) {
118
  const percentage = parseInt(parts[4].replace('%', '')) || 0;
119
  return {
 
124
  };
125
  }
126
  }
127
+ } catch (error) {
128
+ console.warn('Disk usage detection failed:', error.message);
129
  }
130
+
131
  return { total: 0, used: 0, free: 0, percentage: 0 };
132
  }
133
 
134
+ async getProcessList() {
135
+ try {
136
+ let processes = [];
137
+
138
+ if (isWindows) {
139
+ const { stdout } = await execAsync(
140
+ 'powershell "Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, CPU | ConvertTo-Csv -NoTypeInformation"'
141
+ );
142
+
143
+ const lines = stdout.split('\n').slice(1).filter(line => line.trim());
144
+ processes = lines.map(line => {
145
+ const [name, cpu] = line.replace(/"/g, '').split(',');
146
+ return {
147
+ name: name || 'Unknown',
148
+ cpu: parseFloat(cpu) || 0
149
+ };
150
+ }).filter(p => p.name !== 'Unknown').slice(0, 8);
151
+
152
+ } else {
153
+ // Linux/macOS - using ps command
154
+ const { stdout } = await execAsync('ps aux --sort=-%cpu | head -9 | tail -8');
155
+ const lines = stdout.split('\n').filter(line => line.trim());
156
+
157
+ processes = lines.map(line => {
158
+ const parts = line.trim().split(/\s+/);
159
+ return {
160
+ name: parts[10] ? path.basename(parts[10]) : 'unknown',
161
+ cpu: parseFloat(parts[2]) || 0
162
+ };
163
+ });
164
+ }
165
+
166
+ return processes;
167
+ } catch (error) {
168
+ console.warn('Process list detection failed:', error.message);
169
+ return [];
170
+ }
171
+ }
172
+
173
+ async getNetworkStats() {
174
+ try {
175
+ if (isWindows) {
176
+ // Windows network stats - simplified for now
177
+ const { stdout } = await execAsync('powershell "Get-Counter \\"\\Network Interface(*)\\Bytes Total/sec\\" -SampleInterval 1 -MaxSamples 1 | Select-Object -ExpandProperty CounterSamples | Where-Object {$_.InstanceName -ne \\"isatap*\\" -and $_.InstanceName -ne \\"Teredo*\\" -and $_.InstanceName -ne \\"_Total\\""} | Measure-Object CookedValue -Sum | Select-Object -ExpandProperty Sum"');
178
+ const totalBytes = parseFloat(stdout.trim()) || 0;
179
+ return { rx: totalBytes / 2, tx: totalBytes / 2 }; // Rough estimate
180
+ } else {
181
+ // Linux - read from /proc/net/dev
182
+ const data = await fs.readFile('/proc/net/dev', 'utf8');
183
+ const lines = data.split('\n');
184
+ let totalRx = 0, totalTx = 0;
185
+
186
+ for (let line of lines) {
187
+ if (line.includes(':') && !line.includes('lo:')) {
188
+ const parts = line.split(':')[1].trim().split(/\s+/);
189
+ totalRx += parseInt(parts[0]) || 0;
190
+ totalTx += parseInt(parts[8]) || 0;
191
+ }
192
+ }
193
+
194
+ // Calculate speed if we have previous data
195
+ if (this.previousNetworkStats) {
196
+ const rxSpeed = Math.max(0, totalRx - this.previousNetworkStats.rx);
197
+ const txSpeed = Math.max(0, totalTx - this.previousNetworkStats.tx);
198
+ this.previousNetworkStats = { rx: totalRx, tx: totalTx };
199
+ return { rx: rxSpeed, tx: txSpeed };
200
+ } else {
201
+ this.previousNetworkStats = { rx: totalRx, tx: totalTx };
202
+ return { rx: 0, tx: 0 };
203
+ }
204
+ }
205
+ } catch (error) {
206
+ console.warn('Network stats detection failed:', error.message);
207
+ return { rx: 0, tx: 0 };
208
+ }
209
+ }
210
+
211
  formatUptime(seconds) {
212
  const days = Math.floor(seconds / 86400);
213
  const hours = Math.floor((seconds % 86400) / 3600);
214
  const minutes = Math.floor((seconds % 3600) / 60);
215
+
216
  if (days > 0) return `${days}d ${hours}h ${minutes}m`;
217
  if (hours > 0) return `${hours}h ${minutes}m`;
218
  return `${minutes}m`;
219
  }
220
 
221
  async getSystemInfo() {
222
+ try {
223
+ const [cpuUsage, memInfo, diskInfo, processes, networkStats] = await Promise.all([
224
+ this.getCPUUsage(),
225
+ this.getMemoryInfo(),
226
+ this.getDiskUsage(),
227
+ this.getProcessList(),
228
+ this.getNetworkStats()
229
+ ]);
230
+
231
+ const cpuInfo = os.cpus()[0];
232
+ const uptime = os.uptime();
233
+
234
+ return {
235
+ // System info
236
+ platform: os.platform(),
237
+ release: os.release(),
238
+ arch: os.arch(),
239
+ hostname: os.hostname(),
240
+ uptime: this.formatUptime(uptime),
241
+
242
+ // CPU info
243
+ cpu_model: cpuInfo.model,
244
+ cpu_cores: os.cpus().length,
245
+ cpu_usage: cpuUsage,
246
+
247
+ // Memory info
248
+ total_mem: memInfo.total,
249
+ used_mem: memInfo.used,
250
+ free_mem: memInfo.free,
251
+ mem_usage: memInfo.percentage,
252
+
253
+ // Disk info
254
+ disk_total: diskInfo.total,
255
+ disk_used: diskInfo.used,
256
+ disk_free: diskInfo.free,
257
+ disk_usage: diskInfo.percentage,
258
+
259
+ // Network info (speeds in bytes per second)
260
+ network_rx: networkStats.rx,
261
+ network_tx: networkStats.tx,
262
+
263
+ // Process list
264
+ processes: processes,
265
+
266
+ // Load average (Linux/macOS only)
267
+ loadavg: isWindows ? [0, 0, 0] : os.loadavg(),
268
+
269
+ // User info
270
+ user_info: os.userInfo(),
271
+
272
+ timestamp: Date.now()
273
+ };
274
+ } catch (error) {
275
+ console.error('System info error:', error);
276
+ return null;
277
+ }
278
  }
279
  }
280
 
281
  const systemMonitor = new SystemMonitor();
282
 
283
  // === Middleware ===
284
+ app.use(helmet({
285
+ contentSecurityPolicy: {
286
+ directives: {
287
+ defaultSrc: ["'self'"],
288
+ scriptSrc: [
289
+ "'self'",
290
+ "'unsafe-inline'",
291
+ "https://cdn.jsdelivr.net"
292
+ ],
293
+ styleSrc: [
294
+ "'self'",
295
+ "'unsafe-inline'",
296
+ "https://cdn.jsdelivr.net",
297
+ "https://fonts.googleapis.com"
298
+ ],
299
+ fontSrc: [
300
+ "'self'",
301
+ "https://fonts.googleapis.com",
302
+ "https://fonts.gstatic.com"
303
+ ],
304
+ imgSrc: ["'self'", "data:"],
305
+ connectSrc: ["'self'", "ws:", "wss:"]
306
+ }
307
+ },
308
+ crossOriginEmbedderPolicy: false
309
+ }));
310
+
311
  app.use(express.urlencoded({ extended: false }));
312
  app.use(express.json());
313
  app.use(express.static(path.join(__dirname, "public")));
314
  app.set("view engine", "ejs");
315
  app.set("views", path.join(__dirname, "views"));
316
 
317
+ // === Session ===
318
  app.use(
319
  session({
320
  secret: SESSION_SECRET,
321
  resave: false,
322
  saveUninitialized: false,
323
+ cookie: {
324
+ httpOnly: true,
325
+ sameSite: "lax",
326
+ secure: process.env.NODE_ENV === "production",
327
+ maxAge: 24 * 60 * 60 * 1000 // 24 hours
328
+ },
329
  })
330
  );
331
 
 
336
  }
337
 
338
  // === Routes ===
339
+ app.get("/login", (req, res) => {
340
+ if (req.session?.authed) return res.redirect("/");
341
+ res.render("login", { error: null });
342
+ });
343
 
344
  app.post("/login", async (req, res) => {
345
  const { password } = req.body;
346
+ try {
347
+ if (!PASSWORD_HASH) {
348
+ console.error("PASSWORD_HASH not set in environment variables");
349
+ return res.render("login", { error: "Server configuration error" });
350
+ }
351
+
352
+ const isValid = await bcrypt.compare(password || "", PASSWORD_HASH);
353
+ if (isValid) {
354
+ req.session.authed = true;
355
+ return res.redirect("/");
356
+ } else {
357
+ return res.render("login", { error: "Invalid password" });
358
+ }
359
+ } catch (error) {
360
+ console.error("Login error:", error);
361
+ return res.render("login", { error: "Authentication error" });
362
  }
363
  });
364
 
 
366
  req.session.destroy(() => res.redirect("/login"));
367
  });
368
 
369
+ app.get("/", requireAuth, (req, res) => {
370
+ res.render("terminal");
371
+ });
372
 
373
  app.get("/sysinfo", requireAuth, async (req, res) => {
374
  try {
375
+ const systemInfo = await systemMonitor.getSystemInfo();
376
+ if (systemInfo) {
377
+ res.json(systemInfo);
378
+ } else {
379
+ res.status(500).json({ error: "Failed to fetch system information" });
380
+ }
381
+ } catch (error) {
382
+ console.error("System info endpoint error:", error);
383
+ res.status(500).json({ error: "Internal server error" });
384
  }
385
  });
386
 
387
+ // === Terminal Class for better process management ===
388
  class Terminal {
389
  constructor(socket) {
390
  this.socket = socket;
 
394
 
395
  async initialize() {
396
  try {
397
+ // Try to use node-pty first
398
  const nodePty = await import("node-pty");
399
  return this.initWithPty(nodePty);
400
+ } catch (error) {
401
+ console.warn("node-pty not available, using fallback:", error.message);
402
  return this.initWithSpawn();
403
  }
404
  }
405
 
406
  initWithPty(nodePty) {
407
  const shell = this.getShell();
408
+ const env = { ...process.env };
409
+
410
+ // Set better environment for cross-platform compatibility
411
+ if (isWindows) {
412
+ env.TERM = 'windows-ansi';
413
+ } else {
414
+ env.TERM = 'xterm-256color';
415
+ env.COLORTERM = 'truecolor';
416
+ }
417
+
418
  this.process = nodePty.spawn(shell, [], {
419
  name: isWindows ? 'windows-ansi' : 'xterm-256color',
420
  cols: 80,
421
  rows: 24,
422
+ cwd: this.getInitialDirectory(),
423
  env: env,
424
+ useConpty: isWindows, // Use ConPTY on Windows 10+
425
  });
426
 
427
+ this.process.onData(data => {
428
+ if (this.isActive) {
429
+ this.socket.emit("terminal_output", data);
430
+ }
431
+ });
432
 
433
+ this.process.onExit(() => {
434
+ if (this.isActive) {
435
+ this.socket.emit("terminal_output", "\r\n\x1b[91mTerminal session ended\x1b[0m\r\n");
436
+ }
437
+ });
438
 
439
+ // Socket event handlers
440
+ this.socket.on("terminal_input", (data) => {
441
+ if (this.process && this.isActive) {
442
+ this.process.write(data);
443
+ }
444
+ });
445
+
446
+ this.socket.on("terminal_resize", ({ cols, rows }) => {
447
+ if (this.process && this.isActive) {
448
+ this.process.resize(cols || 80, rows || 24);
449
+ }
450
+ });
451
+
452
+ console.log(`Terminal initialized with node-pty (${shell})`);
453
  return true;
454
  }
455
 
456
  initWithSpawn() {
457
  const shell = this.getShell();
458
+ const args = this.getShellArgs();
459
+
460
+ this.process = spawn(shell, args, {
461
+ cwd: this.getInitialDirectory(),
462
+ env: { ...process.env, TERM: isWindows ? 'windows-ansi' : 'xterm-256color' },
463
+ stdio: ['pipe', 'pipe', 'pipe']
464
+ });
465
+
466
+ this.process.stdout.on("data", (data) => {
467
+ if (this.isActive) {
468
+ this.socket.emit("terminal_output", data.toString());
469
+ }
470
+ });
471
+
472
+ this.process.stderr.on("data", (data) => {
473
+ if (this.isActive) {
474
+ this.socket.emit("terminal_output", data.toString());
475
+ }
476
+ });
477
 
478
+ this.process.on("exit", (code) => {
479
+ if (this.isActive) {
480
+ this.socket.emit("terminal_output", `\r\n\x1b[91mProcess exited with code ${code}\x1b[0m\r\n`);
481
+ }
482
+ });
483
+
484
+ this.socket.on("terminal_input", (data) => {
485
+ if (this.process && this.process.stdin && this.isActive) {
486
+ this.process.stdin.write(data);
487
+ }
488
+ });
489
 
490
+ console.log(`Terminal initialized with spawn fallback (${shell})`);
491
  return true;
492
  }
493
 
494
  getShell() {
495
+ if (isWindows) {
496
+ return process.env.COMSPEC || "cmd.exe";
497
+ } else {
498
+ return process.env.SHELL || "/bin/bash";
499
+ }
500
+ }
501
+
502
+ getShellArgs() {
503
+ if (isWindows) {
504
+ return [];
505
+ } else {
506
+ return ["-l"]; // Login shell
507
+ }
508
+ }
509
+
510
+ getInitialDirectory() {
511
+ let dir;
512
+ if (isWindows) {
513
+ dir = process.env.USERPROFILE || process.cwd();
514
+ } else {
515
+ dir = process.env.HOME || process.cwd();
516
+ }
517
+ try {
518
+ require('fs').accessSync(dir, require('fs').constants.R_OK | require('fs').constants.X_OK);
519
+ return dir;
520
+ } catch (e) {
521
+ console.warn(`Initial directory "${dir}" not accessible, fallback to process.cwd()`);
522
+ return process.cwd();
523
+ }
524
  }
525
 
526
  destroy() {
527
  this.isActive = false;
528
  if (this.process) {
529
+ try {
530
+ if (typeof this.process.kill === 'function') {
531
+ this.process.kill();
532
+ } else if (typeof this.process.destroy === 'function') {
533
+ this.process.destroy();
534
+ }
535
+ } catch (error) {
536
+ console.warn("Error destroying terminal process:", error.message);
537
+ }
538
  }
539
  }
540
  }
541
 
542
+ // === Socket.IO Connection Handler ===
543
  io.on("connection", async (socket) => {
544
+ console.log(`Socket connected: ${socket.id} from ${socket.handshake.address}`);
545
+
546
+ // Create terminal instance
547
  const terminal = new Terminal(socket);
548
+ const initSuccess = await terminal.initialize();
549
 
550
+ if (!initSuccess) {
551
+ socket.emit("terminal_output", "\r\n\x1b[91mFailed to initialize terminal\x1b[0m\r\n");
552
+ }
553
+
554
+ // Send welcome message
555
+ const welcomeMessage =
556
+ `\x1b[36mWelcome to Web Terminal\x1b[0m\r\n` +
557
+ `\x1b[90mPlatform:\x1b[0m ${process.platform} (${process.arch})\r\n` +
558
+ `\x1b[90mShell:\x1b[0m ${terminal.getShell()}\r\n` +
559
+ `\x1b[90mType 'help' or 'exit' as needed.\x1b[0m\r\n\r\n`;
560
+
561
+ socket.emit("terminal_output", welcomeMessage);
562
+
563
+ // Real-time system monitoring
564
+ const systemInfoInterval = setInterval(async () => {
565
+ try {
566
+ const systemInfo = await systemMonitor.getSystemInfo();
567
+ if (systemInfo && socket.connected) {
568
+ socket.emit("system_info", systemInfo);
569
+ }
570
+ } catch (error) {
571
+ console.error("System monitoring error:", error);
572
  }
573
  }, 2000);
574
 
575
+ // Cleanup on disconnect
576
+ socket.on("disconnect", () => {
577
+ console.log(`Socket disconnected: ${socket.id}`);
578
+ clearInterval(systemInfoInterval);
579
+ terminal.destroy();
580
+ });
581
+
582
+ // Error handling
583
+ socket.on("error", (error) => {
584
+ console.error(`Socket error for ${socket.id}:`, error);
585
+ });
586
+ });
587
+
588
+ // === Error Handling ===
589
+ process.on('uncaughtException', (error) => {
590
+ console.error('Uncaught Exception:', error);
591
+ });
592
+
593
+ process.on('unhandledRejection', (reason, promise) => {
594
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
595
+ });
596
+
597
+ // === Graceful Shutdown ===
598
+ process.on('SIGTERM', () => {
599
+ console.log('SIGTERM received, shutting down gracefully');
600
+ httpServer.close(() => {
601
+ console.log('Process terminated');
602
+ process.exit(0);
603
+ });
604
+ });
605
+
606
+ process.on('SIGINT', () => {
607
+ console.log('SIGINT received, shutting down gracefully');
608
+ httpServer.close(() => {
609
+ console.log('Process terminated');
610
+ process.exit(0);
611
+ });
612
  });
613
 
614
  // === Start Server ===
615
  httpServer.listen(PORT, () => {
616
+ console.log(`βœ… Web Terminal Server running on http://localhost:${PORT}`);
617
+ console.log(`πŸ“Š Platform: ${process.platform} (${process.arch})`);
618
+ console.log(`🐚 Default Shell: ${isWindows ? process.env.COMSPEC || "cmd.exe" : process.env.SHELL || "/bin/bash"}`);
619
+ console.log(`πŸ” Authentication: ${PASSWORD_HASH ? 'Enabled' : 'Disabled (set WEBTERM_PASSWORD_HASH)'}`);
620
  });
621
+
622
+ export default app;