-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathserver.js
More file actions
681 lines (602 loc) · 24.6 KB
/
Copy pathserver.js
File metadata and controls
681 lines (602 loc) · 24.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
const DEFAULT_PORT = 8080;
const express = require('express');
const { spawn, execSync } = require('child_process');
const path = require('path');
const fs = require('fs').promises;
const os = require('os');
const osUtils = require('os-utils');
require('dotenv').config();
const app = express();
const PORT = 7837;
// Detect platform
const platform = os.platform();
// Middleware
app.use(express.static('public'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Store the running process
let runningProcess = null;
let connectedClients = [];
let lastLaunchConfig = null;
// Create WebSocket server for log streaming
const httpServer = require('http').createServer(app);
const io = require('socket.io')(httpServer, {
cors: {
origin: "*"
}
});
// System monitoring variables
let systemMetrics = {
cpu: { usage: 0, history: [] },
ram: { usage: 0, total: 0, free: 0, history: [] },
gpu: { usage: 0, memory: 0, history: [] },
vram: { usage: 0, total: 0, free: 0, history: [] }
};
// Function to get system metrics
async function getSystemMetrics() {
// CPU Usage using os-utils for more accurate readings
let cpuUsage = 0;
try {
// Use os-utils for better CPU monitoring
cpuUsage = await new Promise((res, rej)=>{
osUtils.cpuUsage((usage)=>{
res(usage * 100);
});
});
} catch (error) {
// Fallback to manual calculation if os-utils fails
const cpus = os.cpus();
let totalIdle = 0;
let totalTick = 0;
cpus.forEach(cpu => {
const times = cpu.times;
totalIdle += times.idle;
totalTick += Object.values(times).reduce((a, b) => a + b, 0);
});
const idlePercentage = (totalIdle / cpus.length) / (totalTick / cpus.length) * 100;
cpuUsage = Math.max(0, 100 - idlePercentage);
}
// RAM Usage
const totalMemory = os.totalmem();
const freeMemory = os.freemem();
const usedMemory = totalMemory - freeMemory;
const ramUsage = (usedMemory / totalMemory) * 100;
// GPU and VRAM Usage - Handle different platforms
let gpuUsage = 0;
let vramUsage = 0;
let vramTotal = 0;
let vramFree = 0;
try {
// Check if we're on macOS (M-chip) or Windows with NVIDIA GPU
if (platform === 'darwin') {
// On macOS, we can't use nvidia-smi, so we'll check for M-chip specifically
console.log('Running on macOS');
// Try to detect if it's an M-chip Mac by checking CPU model
const cpuModel = os.cpus()[0].model;
const isMChip = cpuModel.includes('Apple') || cpuModel.includes('M1') || cpuModel.includes('M2') || cpuModel.includes('M3') || cpuModel.includes('M4');
if (isMChip) {
console.log('Detected Apple M-chip processor - using appropriate GPU monitoring approach');
// For M-chip Macs, we can't easily get GPU usage without additional tools
// We'll use a more realistic simulation for M-chip Macs based on typical usage patterns
gpuUsage = Math.random() * 20 + 5; // Lower usage for Apple M-chip (5-25%)
vramUsage = Math.random() * 40 + 10; // Simulated VRAM usage (10-50%)
vramTotal = 16 * 1024 * 1024 * 1024 / (1024 * 1024); // 16GB in MB (simulated)
vramFree = vramTotal * (1 - vramUsage / 100);
} else {
// Detected Intel-based Mac - using standard GPU simulation
// For Intel-based Macs, we'll also simulate values but with different ranges
gpuUsage = Math.random() * 30; // Lower usage for Intel Mac (simulated)
vramUsage = Math.random() * 50; // Simulated VRAM usage (lower for Mac)
vramTotal = 16 * 1024 * 1024 * 1024 / (1024 * 1024); // 16GB in MB (simulated)
vramFree = vramTotal * (1 - vramUsage / 100);
}
} else if (platform === 'win32') {
// On Windows, try to get NVIDIA GPU data using nvidia-smi
const smiOutput = execSync('nvidia-smi --query-gpu=utilization.gpu,memory.total,memory.used --format=csv,noheader,nounits', { encoding: 'utf8' });
if (smiOutput) {
const lines = smiOutput.trim().split('\n');
if (lines.length > 0) {
const line = lines[0].trim();
const parts = line.split(',').map(p => p.trim());
if (parts.length >= 3) {
gpuUsage = parseFloat(parts[0]) || 0;
vramTotal = parseFloat(parts[1]) || 0;
const vramUsed = parseFloat(parts[2]) || 0;
vramFree = vramTotal - vramUsed;
vramUsage = (vramUsed / vramTotal) * 100 || 0;
}
}
}
} else {
// For other platforms, simulate values
//Running on ${platform} - using simulated GPU data
gpuUsage = Math.random() * 50; // Simulated usage for other platforms
vramUsage = Math.random() * 60; // Simulated VRAM usage
vramTotal = 8 * 1024 * 1024 * 1024 / (1024 * 1024); // 8GB in MB (simulated)
vramFree = vramTotal * (1 - vramUsage / 100);
}
} catch (error) {
// If we fail to get GPU/VRAM data, fall back to simulated values
// Failed to get GPU/VRAM data
gpuUsage = Math.random() * 50; // Simulated fallback for all platforms
vramUsage = Math.random() * 60; // Simulated fallback for all platforms
vramTotal = 8 * 1024 * 1024 * 1024 / (1024 * 1024); // 8GB in MB (simulated)
vramFree = vramTotal * (1 - vramUsage / 100);
}
return {
cpu: cpuUsage,
ram: ramUsage,
gpu: gpuUsage,
vram: vramUsage,
totalMemory: totalMemory,
freeMemory: freeMemory,
vramTotal: vramTotal,
vramFree: vramFree
};
}
// Function to update system metrics history
async function updateSystemMetricsHistory() {
const metrics = await getSystemMetrics();
// Update CPU history (keep last 50 points)
systemMetrics.cpu.usage = metrics.cpu;
if (systemMetrics.cpu.history.length >= 50) {
systemMetrics.cpu.history.shift();
}
systemMetrics.cpu.history.push(metrics.cpu);
// Update RAM history
systemMetrics.ram.usage = metrics.ram;
systemMetrics.ram.total = metrics.totalMemory;
systemMetrics.ram.free = metrics.freeMemory;
if (systemMetrics.ram.history.length >= 50) {
systemMetrics.ram.history.shift();
}
systemMetrics.ram.history.push(metrics.ram);
// Update GPU history
systemMetrics.gpu.usage = metrics.gpu;
systemMetrics.gpu.memory = metrics.vramTotal - metrics.vramFree; // Used VRAM in bytes
if (systemMetrics.gpu.history.length >= 50) {
systemMetrics.gpu.history.shift();
}
systemMetrics.gpu.history.push(metrics.gpu);
// Update VRAM history
systemMetrics.vram.usage = metrics.vram;
systemMetrics.vram.total = metrics.vramTotal;
systemMetrics.vram.free = metrics.vramFree;
if (systemMetrics.vram.history.length >= 50) {
systemMetrics.vram.history.shift();
}
systemMetrics.vram.history.push(metrics.vram);
}
// Start periodic system metrics collection
setInterval(updateSystemMetricsHistory, 1000); // Update every second
// Function to recursively find GGUF files
async function findGGUFFiles(directory) {
const ggufFiles = [];
// Get the models directory from settings or use environment variable or default
let basePath = directory || process.env.MODEL_PATH || "C:\\models";
// Try to read the settings file to get the configured models directory
try {
const settingsPath = path.join(__dirname, 'settings.json');
const settingsData = await fs.readFile(settingsPath, 'utf8');
const settings = JSON.parse(settingsData);
if (settings.modelsDirectory) {
basePath = settings.modelsDirectory;
}
} catch (error) {
// If settings file doesn't exist or can't be read, use default path
console.log('Using default models directory since settings file not found:', error.message);
}
try {
// Check if directory exists
await fs.access(basePath);
async function searchDirectory(dir) {
try {
const items = await fs.readdir(dir, { withFileTypes: true });
for (const item of items) {
const itemPath = path.join(dir, item.name);
if (item.isDirectory()) {
// Recursively search subdirectories
await searchDirectory(itemPath);
} else if (item.isFile() && item.name.toLowerCase().startsWith('mmproj')) {
continue; // Skip mmproj files
}
else if (item.isFile() && item.name.toLowerCase().endsWith('.gguf')) {
// Add GGUF file with relative path
const relativePath = path.relative(basePath, itemPath);
// Check for corresponding mmproj file in the same directory
let mmprojFile = null;
// Look for any file starting with "mmproj" in the same directory
for (const dirItem of items) {
if (dirItem.isFile() && dirItem.name.startsWith('mmproj')) {
mmprojFile = path.join(dir, dirItem.name);
break;
}
}
ggufFiles.push({
name: item.name,
path: itemPath,
relativePath: relativePath,
mmprojFile: mmprojFile
});
}
}
} catch (error) {
console.error(`Error reading directory ${dir}:`, error);
}
}
await searchDirectory(basePath);
} catch (error) {
console.error('Error accessing models directory:', error);
}
return ggufFiles;
}
// Serve the main page
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Serve Swagger documentation
app.get('/swagger', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'swagger.html'));
});
// API endpoint to start the llama server
app.post('/start', (req, res) => {
const { serverPath, args = [] } = req.body;
if (!serverPath) {
return res.status(400).json({
success: false,
error: 'Server path is required'
});
}
// Check if process is already running
if (runningProcess) {
return res.json({
success: false,
error: 'Server is already running'
});
}
// Store the launch configuration for restart functionality
lastLaunchConfig = { serverPath, args };
// Start the server using spawn for better process control
try {
console.log('Starting server with args:', args);
runningProcess = spawn(serverPath, args, { stdio: 'pipe' });
// Handle process events
runningProcess.on('close', (code) => {
console.log(`Server process exited with code ${code}`);
runningProcess = null;
// Notify clients that the process has ended
connectedClients.forEach(client => {
client.emit('server-ended', { message: 'Server process has ended' });
});
});
runningProcess.on('error', (error) => {
console.error(`Failed to start process: ${error}`);
runningProcess = null;
// Notify clients of error
connectedClients.forEach(client => {
client.emit('server-error', { message: 'Failed to start server: ' + error.message });
});
});
// Stream stdout and stderr to connected clients
if (runningProcess.stdout) {
runningProcess.stdout.on('data', (data) => {
const logData = data.toString();
console.log('STDOUT:', logData);
// Broadcast to all connected clients
connectedClients.forEach(client => {
client.emit('log-stream', { type: 'stdout', data: logData });
});
});
}
if (runningProcess.stderr) {
runningProcess.stderr.on('data', (data) => {
const logData = data.toString();
console.log('STDERR:', logData);
// Broadcast to all connected clients
connectedClients.forEach(client => {
client.emit('log-stream', { type: 'stderr', data: logData });
});
});
}
res.json({
success: true,
message: 'Server started successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: `Failed to start server: ${error.message}`
});
}
});
// API endpoint to stop the llama server
app.post('/stop', (req, res) => {
if (!runningProcess) {
return res.json({
success: false,
error: 'No server is currently running'
});
}
// Kill the process gracefully
try {
// Check if process is still running before attempting to kill
if (runningProcess && !runningProcess.killed) {
runningProcess.kill('SIGTERM'); // Try graceful shutdown first
setTimeout(() => {
if (runningProcess && !runningProcess.killed) {
runningProcess.kill('SIGKILL'); // Force kill if still running
}
}, 1000);
}
runningProcess = null;
res.json({
success: true,
message: 'Server stopped successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: `Failed to stop server: ${error.message}`
});
}
});
// API endpoint to get available GGUF models
app.get('/models', async (req, res) => {
try {
const models = await findGGUFFiles();
res.json({
success: true,
models: models
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch models: ' + error.message
});
}
});
// API endpoint to get system metrics
app.get('/metrics', (req, res) => {
// Return just the current values for CPU, RAM, GPU, and VRAM
res.json({
cpu: systemMetrics.cpu.usage,
ram: systemMetrics.ram.usage,
gpu: systemMetrics.gpu.usage,
vram: systemMetrics.vram.usage,
vramUsage: `${systemMetrics.vram.total - systemMetrics.vram.free}/${systemMetrics.vram.total}`,
});
});
// API endpoint to check if server is running
app.get('/status', (req, res) => {
res.json({
running: !!runningProcess && !runningProcess.killed
});
});
// API endpoint to restart the llama server
app.post('/restart', (req, res) => {
// Check if we have a previous launch configuration
if (!lastLaunchConfig) {
return res.status(400).json({
success: false,
error: 'No previous configuration found to restart from'
});
}
// Stop the current server if running
if (runningProcess) {
try {
// Check if process is still running before attempting to kill
if (runningProcess && !runningProcess.killed) {
runningProcess.kill('SIGTERM'); // Try graceful shutdown first
setTimeout(() => {
if (runningProcess && !runningProcess.killed) {
runningProcess.kill('SIGKILL'); // Force kill if still running
}
}, 1000);
}
runningProcess = null;
} catch (error) {
console.error('Error stopping server during restart:', error);
return res.status(500).json({
success: false,
error: `Failed to stop server: ${error.message}`
});
}
}
// Start the server with the last launch configuration
try {
const { serverPath, args } = lastLaunchConfig;
console.log('Restarting server with args:', args);
runningProcess = spawn(serverPath, args, { stdio: 'pipe' });
// Handle process events
runningProcess.on('close', (code) => {
console.log(`Server process exited with code ${code}`);
runningProcess = null;
// Notify clients that the process has ended
connectedClients.forEach(client => {
client.emit('server-ended', { message: 'Server process has ended' });
});
});
runningProcess.on('error', (error) => {
console.error(`Failed to start process: ${error}`);
runningProcess = null;
// Notify clients of error
connectedClients.forEach(client => {
client.emit('server-error', { message: 'Failed to start server: ' + error.message });
});
});
// Stream stdout and stderr to connected clients
if (runningProcess.stdout) {
runningProcess.stdout.on('data', (data) => {
const logData = data.toString();
console.log('STDOUT:', logData);
// Broadcast to all connected clients
connectedClients.forEach(client => {
client.emit('log-stream', { type: 'stdout', data: logData });
});
});
}
if (runningProcess.stderr) {
runningProcess.stderr.on('data', (data) => {
const logData = data.toString();
console.log('STDERR:', logData);
// Broadcast to all connected clients
connectedClients.forEach(client => {
client.emit('log-stream', { type: 'stderr', data: logData });
});
});
}
res.json({
success: true,
message: 'Server restarted successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: `Failed to restart server: ${error.message}`
});
}
});
// WebSocket connection handling for log streaming
io.on('connection', (socket) => {
console.log('Client connected for log streaming');
connectedClients.push(socket);
// Remove client when disconnected
socket.on('disconnect', () => {
console.log('Client disconnected from log streaming');
const index = connectedClients.indexOf(socket);
if (index > -1) {
connectedClients.splice(index, 1);
}
});
});
// Start the server with WebSocket support
httpServer.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
});
// API endpoint to get the current models directory path
app.get('/settings', (req, res) => {
// Read settings from a JSON file or use default if not set
const settingsPath = path.join(__dirname, 'settings.json');
fs.readFile(settingsPath, 'utf8')
.then(data => {
const settings = JSON.parse(data);
res.json({ success: true, settings });
})
.catch(err => {
// If settings file doesn't exist, return default settings
res.json({ success: true, settings: { modelsDirectory: process.env.MODEL_PATH || "C:\\Users\\anubh\\.lmstudio\\models", serverPort: DEFAULT_PORT } });
});
});
// API endpoint to save the models directory path
app.post('/settings', async (req, res) => {
const { modelsDirectory, serverPort } = req.body;
if (!modelsDirectory) {
return res.status(400).json({
success: false,
error: 'Models directory path is required'
});
}
try {
// Validate that the directory exists
await fs.access(modelsDirectory);
// Load existing settings to preserve serverPort
let existingSettings = {};
try {
const existingData = await fs.readFile(settingsPath, 'utf8');
existingSettings = JSON.parse(existingData);
} catch {}
// Save settings to a JSON file
const settingsPath = path.join(__dirname, 'settings.json');
const settings = { modelsDirectory, serverPort: serverPort !== undefined ? serverPort : (existingSettings.serverPort || DEFAULT_PORT) };
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
res.json({
success: true,
message: 'Settings saved successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: `Failed to save settings: ${error.message}`
});
}
});
// API endpoint to launch model presets
app.post('/launch-presets', async (req, res) => {
try {
const { presets, serverPath, serverPort } = req.body;
if (!presets) {
return res.status(400).json({
success: false,
error: 'Presets content is required'
});
}
// Save the presets to a file
const presetsFilePath = path.join(__dirname, 'models-preset.ini');
await fs.writeFile(presetsFilePath, presets);
console.log('Generated models-preset.ini file:', presetsFilePath);
// If no server path found, use default or prompt user
if (!serverPath) {
return res.status(400).json({
success: false,
error: 'Server path not found'
});
}
// Build command arguments for llama-server with --models-preset flag
const args = [];
args.push('--models-preset', presetsFilePath);
args.push('--host', "0.0.0.0");
args.push('--port', (serverPort || DEFAULT_PORT).toString());
console.log('Starting server with presets:', args);
// Start the server using spawn for better process control
runningProcess = spawn(serverPath, args, { stdio: 'pipe' });
// Handle process events
runningProcess.on('close', (code) => {
console.log(`Server process exited with code ${code}`);
runningProcess = null;
// Notify clients that the process has ended
connectedClients.forEach(client => {
client.emit('server-ended', { message: 'Server process has ended' });
});
});
runningProcess.on('error', (error) => {
console.error(`Failed to start process: ${error}`);
runningProcess = null;
// Notify clients of error
connectedClients.forEach(client => {
client.emit('server-error', { message: 'Failed to start server: ' + error.message });
});
});
// Stream stdout and stderr to connected clients
if (runningProcess.stdout) {
runningProcess.stdout.on('data', (data) => {
const logData = data.toString();
console.log('STDOUT:', logData);
// Broadcast to all connected clients
connectedClients.forEach(client => {
client.emit('log-stream', { type: 'stdout', data: logData });
});
});
}
if (runningProcess.stderr) {
runningProcess.stderr.on('data', (data) => {
const logData = data.toString();
console.log('STDERR:', logData);
// Broadcast to all connected clients
connectedClients.forEach(client => {
client.emit('log-stream', { type: 'stderr', data: logData });
});
});
}
res.json({
success: true,
message: 'Server started with model presets successfully',
presetsFile: presetsFilePath
});
} catch (error) {
console.error('Error launching server with presets:', error);
res.status(500).json({
success: false,
error: `Failed to start server with presets: ${error.message}`
});
}
});