dashcam 1.0.1-beta.3 → 1.0.1-beta.31
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.
- package/.github/workflows/publish.yml +26 -20
- package/691cc08dc2fc02f59ae66f08 (1).mp4 +0 -0
- package/NPM_PUBLISH_FIX.md +104 -0
- package/SINGLE_FRAME_VIDEO_FIX.md +129 -0
- package/bin/dashcam-background.js +177 -0
- package/bin/dashcam.js +286 -138
- package/lib/auth.js +5 -5
- package/lib/config.js +1 -1
- package/lib/ffmpeg.js +9 -40
- package/lib/logs/index.js +67 -11
- package/lib/processManager.js +127 -81
- package/lib/recorder.js +153 -33
- package/lib/systemInfo.js +141 -0
- package/lib/tracking/FileTracker.js +8 -1
- package/lib/tracking/LogsTracker.js +21 -7
- package/lib/tracking/icons/index.js +3 -2
- package/lib/tracking/icons/linux.js +370 -0
- package/lib/uploader.js +23 -10
- package/package.json +5 -1
- package/scripts/sync-version.sh +48 -0
- package/test-short-recording.js +287 -0
- package/test-system-info.js +105 -0
- package/test_workflow.sh +110 -27
- package/.github/workflows/build.yml +0 -103
- package/.github/workflows/release.yml +0 -107
- package/sea-bundle.mjs +0 -34595
package/lib/recorder.js
CHANGED
|
@@ -4,10 +4,52 @@ import { createGif, createSnapshot } from './ffmpeg.js';
|
|
|
4
4
|
import { applicationTracker } from './applicationTracker.js';
|
|
5
5
|
import { logsTrackerManager, trimLogs } from './logs/index.js';
|
|
6
6
|
import { getFfmpegPath } from './binaries.js';
|
|
7
|
+
import { APP } from './config.js';
|
|
7
8
|
import path from 'path';
|
|
8
9
|
import os from 'os';
|
|
9
10
|
import fs from 'fs';
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Fix/repair a video file by re-muxing it with proper container metadata
|
|
14
|
+
* This copies streams without re-encoding and ensures proper container finalization
|
|
15
|
+
*/
|
|
16
|
+
async function fixVideoContainer(inputVideoPath, outputVideoPath) {
|
|
17
|
+
const logExit = logFunctionCall('fixVideoContainer', { inputVideoPath, outputVideoPath });
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const ffmpegPath = await getFfmpegPath();
|
|
21
|
+
|
|
22
|
+
logger.info('Re-muxing video to fix container metadata', {
|
|
23
|
+
input: inputVideoPath,
|
|
24
|
+
output: outputVideoPath
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const args = [
|
|
28
|
+
'-i', inputVideoPath,
|
|
29
|
+
'-vcodec', 'copy', // Copy video stream without re-encoding
|
|
30
|
+
'-acodec', 'copy', // Copy audio stream without re-encoding
|
|
31
|
+
'-movflags', 'faststart', // Enable fast start for web playback
|
|
32
|
+
outputVideoPath,
|
|
33
|
+
'-y', // Overwrite output file
|
|
34
|
+
'-hide_banner'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
await execa(ffmpegPath, args);
|
|
38
|
+
|
|
39
|
+
logger.info('Successfully re-muxed video', {
|
|
40
|
+
outputPath: outputVideoPath,
|
|
41
|
+
outputSize: fs.existsSync(outputVideoPath) ? fs.statSync(outputVideoPath).size : 0
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
logExit();
|
|
45
|
+
return true;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger.error('Failed to fix video container', { error: error.message });
|
|
48
|
+
logExit();
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
11
53
|
/**
|
|
12
54
|
* Dynamically detect the primary screen capture device for the current platform
|
|
13
55
|
*/
|
|
@@ -104,8 +146,8 @@ let outputPath = null;
|
|
|
104
146
|
let recordingStartTime = null;
|
|
105
147
|
let currentTempFile = null;
|
|
106
148
|
|
|
107
|
-
// File paths - use
|
|
108
|
-
const DASHCAM_TEMP_DIR =
|
|
149
|
+
// File paths - use APP config directory for better Windows compatibility
|
|
150
|
+
const DASHCAM_TEMP_DIR = APP.configDir;
|
|
109
151
|
const TEMP_FILE_INFO_PATH = path.join(DASHCAM_TEMP_DIR, 'temp-file.json');
|
|
110
152
|
|
|
111
153
|
// Platform-specific configurations
|
|
@@ -116,9 +158,8 @@ const PLATFORM_CONFIG = {
|
|
|
116
158
|
audioInput: '0', // Default audio device if needed
|
|
117
159
|
audioFormat: 'avfoundation',
|
|
118
160
|
extraArgs: [
|
|
119
|
-
'-
|
|
120
|
-
'-
|
|
121
|
-
'-r', '30' // Set frame rate
|
|
161
|
+
'-capture_cursor', '1', // Capture mouse cursor
|
|
162
|
+
'-capture_mouse_clicks', '1' // Capture mouse clicks
|
|
122
163
|
]
|
|
123
164
|
},
|
|
124
165
|
win32: {
|
|
@@ -160,6 +201,7 @@ async function getPlatformArgs({ fps, includeAudio }) {
|
|
|
160
201
|
});
|
|
161
202
|
|
|
162
203
|
const args = [
|
|
204
|
+
'-thread_queue_size', '512', // Increase input buffer to prevent drops
|
|
163
205
|
'-f', config.inputFormat
|
|
164
206
|
];
|
|
165
207
|
|
|
@@ -170,12 +212,14 @@ async function getPlatformArgs({ fps, includeAudio }) {
|
|
|
170
212
|
|
|
171
213
|
args.push(
|
|
172
214
|
'-framerate', fps.toString(),
|
|
215
|
+
'-probesize', '50M', // Increase probe size for better stream detection
|
|
173
216
|
'-i', screenInput
|
|
174
217
|
);
|
|
175
218
|
|
|
176
219
|
// Add audio capture if enabled
|
|
177
220
|
if (includeAudio) {
|
|
178
221
|
args.push(
|
|
222
|
+
'-thread_queue_size', '512', // Increase audio buffer too
|
|
179
223
|
'-f', config.audioFormat,
|
|
180
224
|
'-i', config.audioInput
|
|
181
225
|
);
|
|
@@ -195,12 +239,12 @@ async function getPlatformArgs({ fps, includeAudio }) {
|
|
|
195
239
|
}
|
|
196
240
|
|
|
197
241
|
/**
|
|
198
|
-
* Clear the
|
|
242
|
+
* Clear the recordings directory
|
|
199
243
|
*/
|
|
200
244
|
function clearRecordingsDirectory() {
|
|
201
245
|
const logExit = logFunctionCall('clearRecordingsDirectory');
|
|
202
246
|
|
|
203
|
-
const directory =
|
|
247
|
+
const directory = APP.recordingsDir;
|
|
204
248
|
|
|
205
249
|
try {
|
|
206
250
|
if (fs.existsSync(directory)) {
|
|
@@ -238,8 +282,8 @@ function generateOutputPath() {
|
|
|
238
282
|
const logExit = logFunctionCall('generateOutputPath');
|
|
239
283
|
|
|
240
284
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
241
|
-
// Use
|
|
242
|
-
const directory =
|
|
285
|
+
// Use APP recordings directory for consistent cross-platform location
|
|
286
|
+
const directory = APP.recordingsDir;
|
|
243
287
|
const filepath = path.join(directory, `recording-${timestamp}.webm`);
|
|
244
288
|
|
|
245
289
|
logger.verbose('Generating output path', {
|
|
@@ -278,15 +322,21 @@ export async function startRecording({
|
|
|
278
322
|
// Construct FFmpeg command arguments
|
|
279
323
|
const platformArgs = await getPlatformArgs({ fps, includeAudio });
|
|
280
324
|
const outputArgs = [
|
|
281
|
-
'-c:v', 'libvpx',
|
|
282
|
-
'-
|
|
283
|
-
//
|
|
325
|
+
'-c:v', 'libvpx-vp9', // Use VP9 codec for better quality and compression
|
|
326
|
+
'-quality', 'realtime', // Use realtime quality preset for faster encoding
|
|
327
|
+
'-cpu-used', '8', // Maximum speed (0-8, higher = faster but lower quality)
|
|
328
|
+
'-deadline', 'realtime',// Realtime encoding mode for lowest latency
|
|
329
|
+
'-b:v', '2M', // Target bitrate
|
|
284
330
|
'-r', fps.toString(), // Ensure output framerate matches input
|
|
285
|
-
'-g',
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
'-
|
|
289
|
-
'-
|
|
331
|
+
'-g', fps.toString(), // Keyframe interval = 1 second (every fps frames) - ensures frequent keyframes
|
|
332
|
+
'-force_key_frames', `expr:gte(t,n_forced*1)`, // Force keyframe every 1 second
|
|
333
|
+
// WebM options for more frequent disk writes and proper stream handling
|
|
334
|
+
'-f', 'webm', // Force WebM container format
|
|
335
|
+
'-flush_packets', '1', // Flush packets immediately to disk - critical for short recordings
|
|
336
|
+
'-fflags', '+genpts', // Generate presentation timestamps
|
|
337
|
+
'-avoid_negative_ts', 'make_zero', // Avoid negative timestamps
|
|
338
|
+
'-vsync', '1', // Ensure every frame is encoded (CFR - constant frame rate)
|
|
339
|
+
'-max_muxing_queue_size', '9999' // Large queue to prevent frame drops
|
|
290
340
|
];
|
|
291
341
|
|
|
292
342
|
if (includeAudio) {
|
|
@@ -358,13 +408,11 @@ export async function startRecording({
|
|
|
358
408
|
all: true, // Capture both stdout and stderr
|
|
359
409
|
stdin: 'pipe' // Enable stdin for sending 'q' to stop recording
|
|
360
410
|
});
|
|
361
|
-
|
|
362
|
-
recordingStartTime = Date.now();
|
|
363
411
|
|
|
364
412
|
logger.info('FFmpeg process spawned', {
|
|
365
413
|
pid: currentRecording.pid,
|
|
366
414
|
args: args.slice(-5), // Log last 5 args including output file
|
|
367
|
-
tempFile
|
|
415
|
+
tempFile
|
|
368
416
|
});
|
|
369
417
|
|
|
370
418
|
// Check if temp file gets created within first few seconds
|
|
@@ -392,6 +440,14 @@ export async function startRecording({
|
|
|
392
440
|
directory: path.dirname(outputPath)
|
|
393
441
|
});
|
|
394
442
|
|
|
443
|
+
// Set recording start time AFTER log tracker is initialized
|
|
444
|
+
// This ensures the timeline starts when the tracker is ready to capture events
|
|
445
|
+
recordingStartTime = Date.now();
|
|
446
|
+
logger.info('Recording timeline started', {
|
|
447
|
+
recordingStartTime,
|
|
448
|
+
recordingStartTimeReadable: new Date(recordingStartTime).toISOString()
|
|
449
|
+
});
|
|
450
|
+
|
|
395
451
|
if (currentRecording.all) {
|
|
396
452
|
currentRecording.all.setEncoding('utf8');
|
|
397
453
|
currentRecording.all.on('data', (data) => {
|
|
@@ -456,29 +512,40 @@ export async function stopRecording() {
|
|
|
456
512
|
duration: recordingDuration,
|
|
457
513
|
durationSeconds: (recordingDuration / 1000).toFixed(1)
|
|
458
514
|
});
|
|
515
|
+
|
|
516
|
+
// Enforce minimum recording duration to prevent single-frame videos
|
|
517
|
+
const MIN_RECORDING_DURATION = 2000; // 2 seconds minimum
|
|
518
|
+
if (recordingDuration < MIN_RECORDING_DURATION) {
|
|
519
|
+
const waitTime = MIN_RECORDING_DURATION - recordingDuration;
|
|
520
|
+
logger.info(`Recording too short (${recordingDuration}ms), waiting ${waitTime}ms to ensure multiple frames`);
|
|
521
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
522
|
+
}
|
|
459
523
|
|
|
460
524
|
try {
|
|
461
525
|
// First try to gracefully stop FFmpeg by sending 'q'
|
|
462
526
|
if (currentRecording && currentRecording.stdin) {
|
|
463
527
|
logger.debug('Sending quit signal to FFmpeg...');
|
|
464
528
|
currentRecording.stdin.write('q');
|
|
529
|
+
currentRecording.stdin.end(); // Close stdin to signal end
|
|
465
530
|
}
|
|
466
531
|
|
|
467
|
-
// Wait for FFmpeg to finish gracefully
|
|
532
|
+
// Wait for FFmpeg to finish gracefully with realtime encoding
|
|
468
533
|
const gracefulTimeout = setTimeout(() => {
|
|
469
534
|
if (currentRecording && !currentRecording.killed) {
|
|
535
|
+
logger.warn('FFmpeg did not exit gracefully after 10s, sending SIGTERM...');
|
|
470
536
|
// If still running, try SIGTERM
|
|
471
537
|
process.kill(currentRecording.pid, 'SIGTERM');
|
|
472
538
|
}
|
|
473
|
-
},
|
|
539
|
+
}, 10000); // Wait longer for graceful shutdown
|
|
474
540
|
|
|
475
|
-
// Wait up to
|
|
541
|
+
// Wait up to 20 seconds for SIGTERM to work
|
|
476
542
|
const hardKillTimeout = setTimeout(() => {
|
|
477
543
|
if (currentRecording && !currentRecording.killed) {
|
|
544
|
+
logger.error('FFmpeg still running after SIGTERM, using SIGKILL...');
|
|
478
545
|
// If still not dead, use SIGKILL as last resort
|
|
479
546
|
process.kill(currentRecording.pid, 'SIGKILL');
|
|
480
547
|
}
|
|
481
|
-
},
|
|
548
|
+
}, 20000); // Longer timeout to avoid killing prematurely
|
|
482
549
|
|
|
483
550
|
// Wait for the process to fully exit
|
|
484
551
|
if (currentRecording) {
|
|
@@ -489,8 +556,30 @@ export async function stopRecording() {
|
|
|
489
556
|
clearTimeout(gracefulTimeout);
|
|
490
557
|
clearTimeout(hardKillTimeout);
|
|
491
558
|
|
|
492
|
-
//
|
|
493
|
-
|
|
559
|
+
// Wait for filesystem to sync and file to be written
|
|
560
|
+
// Poll for the file to exist with content
|
|
561
|
+
logger.debug('Waiting for temp file to be written...');
|
|
562
|
+
const maxWaitTime = 10000; // Wait up to 10 seconds
|
|
563
|
+
const startWait = Date.now();
|
|
564
|
+
let tempFileReady = false;
|
|
565
|
+
|
|
566
|
+
while (!tempFileReady && (Date.now() - startWait) < maxWaitTime) {
|
|
567
|
+
const tempFile = currentTempFile;
|
|
568
|
+
if (tempFile && fs.existsSync(tempFile)) {
|
|
569
|
+
const stats = fs.statSync(tempFile);
|
|
570
|
+
if (stats.size > 0) {
|
|
571
|
+
logger.debug('Temp file is ready', { size: stats.size });
|
|
572
|
+
tempFileReady = true;
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// Wait a bit before checking again
|
|
577
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (!tempFileReady) {
|
|
581
|
+
logger.warn('Temp file not ready after waiting, proceeding anyway');
|
|
582
|
+
}
|
|
494
583
|
|
|
495
584
|
// Read temp file path from disk (for cross-process access)
|
|
496
585
|
let tempFile = currentTempFile; // Try in-memory first
|
|
@@ -538,21 +627,41 @@ export async function stopRecording() {
|
|
|
538
627
|
path: tempFile
|
|
539
628
|
});
|
|
540
629
|
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
logger.debug('Copying temp file to final output...');
|
|
630
|
+
// Re-mux to ensure proper container metadata (duration, seekability, etc.)
|
|
631
|
+
logger.debug('Re-muxing temp file to fix container metadata...');
|
|
544
632
|
|
|
545
633
|
try {
|
|
546
|
-
|
|
547
|
-
|
|
634
|
+
// First, create a temporary fixed version
|
|
635
|
+
const fixedTempFile = tempFile.replace('.webm', '-fixed.webm');
|
|
636
|
+
|
|
637
|
+
const fixSuccess = await fixVideoContainer(tempFile, fixedTempFile);
|
|
638
|
+
|
|
639
|
+
if (fixSuccess && fs.existsSync(fixedTempFile) && fs.statSync(fixedTempFile).size > 0) {
|
|
640
|
+
// Use the fixed version
|
|
641
|
+
logger.info('Using re-muxed version with proper container metadata');
|
|
642
|
+
fs.copyFileSync(fixedTempFile, outputPath);
|
|
643
|
+
|
|
644
|
+
// Clean up the fixed temp file
|
|
645
|
+
try {
|
|
646
|
+
fs.unlinkSync(fixedTempFile);
|
|
647
|
+
} catch (e) {
|
|
648
|
+
logger.debug('Failed to delete fixed temp file:', e);
|
|
649
|
+
}
|
|
650
|
+
} else {
|
|
651
|
+
// Fallback: just copy the original temp file
|
|
652
|
+
logger.warn('Re-muxing failed, using original file');
|
|
653
|
+
fs.copyFileSync(tempFile, outputPath);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
logger.info('Successfully finalized recording to output');
|
|
548
657
|
|
|
549
658
|
// Verify the final file exists and has content
|
|
550
659
|
if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size === 0) {
|
|
551
|
-
throw new Error('Final output file is empty or missing after
|
|
660
|
+
throw new Error('Final output file is empty or missing after processing');
|
|
552
661
|
}
|
|
553
662
|
|
|
554
663
|
} catch (error) {
|
|
555
|
-
logger.error('Failed to
|
|
664
|
+
logger.error('Failed to process temp file:', error);
|
|
556
665
|
throw new Error('Failed to finalize recording: ' + error.message);
|
|
557
666
|
}
|
|
558
667
|
|
|
@@ -622,6 +731,12 @@ export async function stopRecording() {
|
|
|
622
731
|
icons: appTrackingResults.icons, // Include application icons metadata
|
|
623
732
|
logs: logTrackingResults // Include log tracking results
|
|
624
733
|
};
|
|
734
|
+
|
|
735
|
+
logger.info('Recording stopped with clientStartDate', {
|
|
736
|
+
clientStartDate: recordingStartTime,
|
|
737
|
+
clientStartDateReadable: new Date(recordingStartTime).toISOString(),
|
|
738
|
+
duration: result.duration
|
|
739
|
+
});
|
|
625
740
|
|
|
626
741
|
currentRecording = null;
|
|
627
742
|
recordingStartTime = null;
|
|
@@ -673,3 +788,8 @@ export function getRecordingStatus() {
|
|
|
673
788
|
outputPath
|
|
674
789
|
};
|
|
675
790
|
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Export the fix function for external use
|
|
794
|
+
*/
|
|
795
|
+
export { fixVideoContainer };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import si from 'systeminformation';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Collects comprehensive system information including CPU, memory, OS, and graphics data.
|
|
6
|
+
* This matches the data format expected by the Dashcam backend (same as desktop app).
|
|
7
|
+
*
|
|
8
|
+
* @returns {Promise<Object>} System information object
|
|
9
|
+
*/
|
|
10
|
+
export async function getSystemInfo() {
|
|
11
|
+
try {
|
|
12
|
+
logger.debug('Collecting system information...');
|
|
13
|
+
|
|
14
|
+
// Collect only essential system information quickly
|
|
15
|
+
// Graphics info can be very slow, so we skip it or use a short timeout
|
|
16
|
+
const [cpu, mem, osInfo, system] = await Promise.all([
|
|
17
|
+
si.cpu(),
|
|
18
|
+
si.mem(),
|
|
19
|
+
si.osInfo(),
|
|
20
|
+
si.system()
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// Try to get graphics info with a very short timeout (optional)
|
|
24
|
+
let graphics = { controllers: [], displays: [] };
|
|
25
|
+
try {
|
|
26
|
+
graphics = await Promise.race([
|
|
27
|
+
si.graphics(),
|
|
28
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Graphics timeout')), 2000))
|
|
29
|
+
]);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
logger.debug('Graphics info timed out, using empty graphics data');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const systemInfo = {
|
|
35
|
+
cpu: {
|
|
36
|
+
manufacturer: cpu.manufacturer,
|
|
37
|
+
brand: cpu.brand,
|
|
38
|
+
vendor: cpu.vendor,
|
|
39
|
+
family: cpu.family,
|
|
40
|
+
model: cpu.model,
|
|
41
|
+
stepping: cpu.stepping,
|
|
42
|
+
revision: cpu.revision,
|
|
43
|
+
voltage: cpu.voltage,
|
|
44
|
+
speed: cpu.speed,
|
|
45
|
+
speedMin: cpu.speedMin,
|
|
46
|
+
speedMax: cpu.speedMax,
|
|
47
|
+
cores: cpu.cores,
|
|
48
|
+
physicalCores: cpu.physicalCores,
|
|
49
|
+
processors: cpu.processors,
|
|
50
|
+
socket: cpu.socket,
|
|
51
|
+
cache: cpu.cache
|
|
52
|
+
},
|
|
53
|
+
mem: {
|
|
54
|
+
total: mem.total,
|
|
55
|
+
free: mem.free,
|
|
56
|
+
used: mem.used,
|
|
57
|
+
active: mem.active,
|
|
58
|
+
available: mem.available,
|
|
59
|
+
swaptotal: mem.swaptotal,
|
|
60
|
+
swapused: mem.swapused,
|
|
61
|
+
swapfree: mem.swapfree
|
|
62
|
+
},
|
|
63
|
+
os: {
|
|
64
|
+
platform: osInfo.platform,
|
|
65
|
+
distro: osInfo.distro,
|
|
66
|
+
release: osInfo.release,
|
|
67
|
+
codename: osInfo.codename,
|
|
68
|
+
kernel: osInfo.kernel,
|
|
69
|
+
arch: osInfo.arch,
|
|
70
|
+
hostname: osInfo.hostname,
|
|
71
|
+
fqdn: osInfo.fqdn,
|
|
72
|
+
codepage: osInfo.codepage,
|
|
73
|
+
logofile: osInfo.logofile,
|
|
74
|
+
build: osInfo.build,
|
|
75
|
+
servicepack: osInfo.servicepack,
|
|
76
|
+
uefi: osInfo.uefi
|
|
77
|
+
},
|
|
78
|
+
graphics: {
|
|
79
|
+
controllers: graphics.controllers?.map(controller => ({
|
|
80
|
+
vendor: controller.vendor,
|
|
81
|
+
model: controller.model,
|
|
82
|
+
bus: controller.bus,
|
|
83
|
+
vram: controller.vram,
|
|
84
|
+
vramDynamic: controller.vramDynamic
|
|
85
|
+
})),
|
|
86
|
+
displays: graphics.displays?.map(display => ({
|
|
87
|
+
vendor: display.vendor,
|
|
88
|
+
model: display.model,
|
|
89
|
+
main: display.main,
|
|
90
|
+
builtin: display.builtin,
|
|
91
|
+
connection: display.connection,
|
|
92
|
+
resolutionX: display.resolutionX,
|
|
93
|
+
resolutionY: display.resolutionY,
|
|
94
|
+
sizeX: display.sizeX,
|
|
95
|
+
sizeY: display.sizeY,
|
|
96
|
+
pixelDepth: display.pixelDepth,
|
|
97
|
+
currentResX: display.currentResX,
|
|
98
|
+
currentResY: display.currentResY,
|
|
99
|
+
currentRefreshRate: display.currentRefreshRate
|
|
100
|
+
}))
|
|
101
|
+
},
|
|
102
|
+
system: {
|
|
103
|
+
manufacturer: system.manufacturer,
|
|
104
|
+
model: system.model,
|
|
105
|
+
version: system.version,
|
|
106
|
+
serial: system.serial,
|
|
107
|
+
uuid: system.uuid,
|
|
108
|
+
sku: system.sku,
|
|
109
|
+
virtual: system.virtual
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
logger.verbose('System information collected', {
|
|
114
|
+
cpuBrand: cpu.brand,
|
|
115
|
+
totalMemoryGB: (mem.total / (1024 * 1024 * 1024)).toFixed(2),
|
|
116
|
+
os: `${osInfo.distro} ${osInfo.release}`,
|
|
117
|
+
graphicsControllers: graphics.controllers?.length || 0,
|
|
118
|
+
displays: graphics.displays?.length || 0
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return systemInfo;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logger.error('Failed to collect system information:', {
|
|
124
|
+
message: error.message,
|
|
125
|
+
stack: error.stack
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Return minimal system info as fallback
|
|
129
|
+
return {
|
|
130
|
+
cpu: { brand: 'Unknown', cores: 0 },
|
|
131
|
+
mem: { total: 0 },
|
|
132
|
+
os: {
|
|
133
|
+
platform: process.platform,
|
|
134
|
+
arch: process.arch,
|
|
135
|
+
release: process.version
|
|
136
|
+
},
|
|
137
|
+
graphics: { controllers: [], displays: [] },
|
|
138
|
+
system: { manufacturer: 'Unknown', model: 'Unknown' }
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Tail } from 'tail';
|
|
2
2
|
import { logger } from '../logger.js';
|
|
3
|
+
import fs from 'fs';
|
|
3
4
|
|
|
4
5
|
// Simple function to get stats for events in the last minute
|
|
5
6
|
function getStats(eventTimes = []) {
|
|
@@ -40,7 +41,13 @@ export class FileTracker {
|
|
|
40
41
|
this.trackedFile = trackedFile;
|
|
41
42
|
|
|
42
43
|
try {
|
|
43
|
-
|
|
44
|
+
// Ensure the file exists before creating the Tail watcher
|
|
45
|
+
if (!fs.existsSync(this.trackedFile)) {
|
|
46
|
+
logger.warn(`File does not exist, creating: ${this.trackedFile}`);
|
|
47
|
+
fs.writeFileSync(this.trackedFile, '', 'utf8');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.tail = new Tail(this.trackedFile, { encoding: 'utf8' });
|
|
44
51
|
this.tail.on('line', (line) => {
|
|
45
52
|
const time = Date.now();
|
|
46
53
|
this.eventTimes.push(time);
|
|
@@ -55,6 +55,7 @@ export class LogsTracker {
|
|
|
55
55
|
|
|
56
56
|
this.files[filePath] = {
|
|
57
57
|
status,
|
|
58
|
+
filePath, // Store file path for later reference
|
|
58
59
|
unsubscribe: this.fileTrackerManager.subscribe(filePath, callback),
|
|
59
60
|
};
|
|
60
61
|
|
|
@@ -98,13 +99,25 @@ export class LogsTracker {
|
|
|
98
99
|
|
|
99
100
|
getStatus() {
|
|
100
101
|
let items = [];
|
|
102
|
+
let filePathMap = {};
|
|
103
|
+
|
|
101
104
|
if (this.isWatchOnly) {
|
|
102
|
-
items = Object.keys(this.files).map((filePath) =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
items = Object.keys(this.files).map((filePath) => {
|
|
106
|
+
const index = this.fileToIndex[filePath];
|
|
107
|
+
filePathMap[index] = filePath;
|
|
108
|
+
return {
|
|
109
|
+
...this.fileTrackerManager.getStats(filePath),
|
|
110
|
+
item: index, // Keep numeric index to match events
|
|
111
|
+
};
|
|
112
|
+
});
|
|
106
113
|
} else {
|
|
107
|
-
items = Object.values(this.files).map(({ status }) =>
|
|
114
|
+
items = Object.values(this.files).map(({ status, filePath }) => {
|
|
115
|
+
const index = status.item;
|
|
116
|
+
filePathMap[index] = filePath;
|
|
117
|
+
return {
|
|
118
|
+
...status,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
const totalCount = items.reduce((acc, status) => acc + status.count, 0);
|
|
@@ -112,11 +125,12 @@ export class LogsTracker {
|
|
|
112
125
|
return [
|
|
113
126
|
{
|
|
114
127
|
id: 'CLI',
|
|
115
|
-
name: '
|
|
128
|
+
name: 'File Logs', // More descriptive name
|
|
116
129
|
type: 'cli',
|
|
117
130
|
fileLocation: this.fileLocation,
|
|
118
131
|
items: items,
|
|
119
132
|
count: totalCount,
|
|
133
|
+
filePathMap: filePathMap, // Include mapping for UI to display file paths
|
|
120
134
|
},
|
|
121
135
|
];
|
|
122
136
|
}
|
|
@@ -126,7 +140,7 @@ export class LogsTracker {
|
|
|
126
140
|
const status = this.getStatus();
|
|
127
141
|
return status.length > 0 ? status[0] : {
|
|
128
142
|
id: 'CLI',
|
|
129
|
-
name: '
|
|
143
|
+
name: 'File Logs',
|
|
130
144
|
type: 'cli',
|
|
131
145
|
fileLocation: this.fileLocation,
|
|
132
146
|
items: [],
|
|
@@ -20,8 +20,9 @@ async function ensureIconModule() {
|
|
|
20
20
|
const windowsModule = await import("./windows.js");
|
|
21
21
|
getIconAsBuffer = windowsModule.getIconAsBuffer;
|
|
22
22
|
} else {
|
|
23
|
-
// Linux
|
|
24
|
-
|
|
23
|
+
// Linux support
|
|
24
|
+
const linuxModule = await import("./linux.js");
|
|
25
|
+
getIconAsBuffer = linuxModule.getIconAsBuffer;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
iconModuleLoaded = true;
|