dashcam 1.0.1-beta.9 → 1.0.2-beta.1
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.js +95 -96
- package/lib/auth.js +5 -5
- package/lib/config.js +1 -1
- package/lib/ffmpeg.js +9 -39
- package/lib/processManager.js +23 -36
- package/lib/recorder.js +130 -26
- package/lib/systemInfo.js +141 -0
- package/lib/tracking/FileTracker.js +1 -1
- package/lib/tracking/icons/index.js +3 -2
- package/lib/tracking/icons/linux.js +370 -0
- package/lib/uploader.js +13 -7
- package/package.json +2 -1
- package/scripts/sync-version.sh +48 -0
- package/test-short-recording.js +287 -0
- package/test_workflow.sh +19 -14
- package/BACKWARD_COMPATIBILITY.md +0 -177
package/lib/processManager.js
CHANGED
|
@@ -160,8 +160,9 @@ class ProcessManager {
|
|
|
160
160
|
logger.info('Stopping active recording process', { pid });
|
|
161
161
|
process.kill(pid, 'SIGINT');
|
|
162
162
|
|
|
163
|
-
// Wait for the process to actually finish
|
|
164
|
-
|
|
163
|
+
// Wait for the process to actually finish and upload
|
|
164
|
+
// Increase timeout to allow for upload to complete
|
|
165
|
+
const maxWaitTime = 120000; // 2 minutes max to allow for upload
|
|
165
166
|
const startWait = Date.now();
|
|
166
167
|
|
|
167
168
|
while (this.isProcessRunning(pid) && (Date.now() - startWait) < maxWaitTime) {
|
|
@@ -174,41 +175,27 @@ class ProcessManager {
|
|
|
174
175
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
//
|
|
178
|
+
// The background process handles stopRecording() internally via SIGINT
|
|
179
|
+
// We just need to return the basic result from the status file
|
|
178
180
|
if (status) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Fallback to basic result if recorder fails
|
|
198
|
-
const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
|
|
199
|
-
const result = {
|
|
200
|
-
outputPath: status.outputPath,
|
|
201
|
-
gifPath: `${basePath}.gif`,
|
|
202
|
-
snapshotPath: `${basePath}.png`,
|
|
203
|
-
duration: Date.now() - status.startTime,
|
|
204
|
-
clientStartDate: status.startTime,
|
|
205
|
-
apps: [],
|
|
206
|
-
logs: []
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
this.cleanup({ preserveResult: true });
|
|
210
|
-
return result;
|
|
211
|
-
}
|
|
181
|
+
logger.info('Background recording stopped, returning status', {
|
|
182
|
+
outputPath: status.outputPath,
|
|
183
|
+
duration: Date.now() - status.startTime
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
|
|
187
|
+
const result = {
|
|
188
|
+
outputPath: status.outputPath,
|
|
189
|
+
gifPath: `${basePath}.gif`,
|
|
190
|
+
snapshotPath: `${basePath}.png`,
|
|
191
|
+
duration: Date.now() - status.startTime,
|
|
192
|
+
clientStartDate: status.startTime,
|
|
193
|
+
apps: [],
|
|
194
|
+
logs: []
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
this.cleanup({ preserveResult: true });
|
|
198
|
+
return result;
|
|
212
199
|
} else {
|
|
213
200
|
throw new Error('No status information available for active recording');
|
|
214
201
|
}
|
package/lib/recorder.js
CHANGED
|
@@ -8,6 +8,47 @@ import path from 'path';
|
|
|
8
8
|
import os from 'os';
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Fix/repair a video file by re-muxing it with proper container metadata
|
|
13
|
+
* This copies streams without re-encoding and ensures proper container finalization
|
|
14
|
+
*/
|
|
15
|
+
async function fixVideoContainer(inputVideoPath, outputVideoPath) {
|
|
16
|
+
const logExit = logFunctionCall('fixVideoContainer', { inputVideoPath, outputVideoPath });
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const ffmpegPath = await getFfmpegPath();
|
|
20
|
+
|
|
21
|
+
logger.info('Re-muxing video to fix container metadata', {
|
|
22
|
+
input: inputVideoPath,
|
|
23
|
+
output: outputVideoPath
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const args = [
|
|
27
|
+
'-i', inputVideoPath,
|
|
28
|
+
'-vcodec', 'copy', // Copy video stream without re-encoding
|
|
29
|
+
'-acodec', 'copy', // Copy audio stream without re-encoding
|
|
30
|
+
'-movflags', 'faststart', // Enable fast start for web playback
|
|
31
|
+
outputVideoPath,
|
|
32
|
+
'-y', // Overwrite output file
|
|
33
|
+
'-hide_banner'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
await execa(ffmpegPath, args);
|
|
37
|
+
|
|
38
|
+
logger.info('Successfully re-muxed video', {
|
|
39
|
+
outputPath: outputVideoPath,
|
|
40
|
+
outputSize: fs.existsSync(outputVideoPath) ? fs.statSync(outputVideoPath).size : 0
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
logExit();
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error('Failed to fix video container', { error: error.message });
|
|
47
|
+
logExit();
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
11
52
|
/**
|
|
12
53
|
* Dynamically detect the primary screen capture device for the current platform
|
|
13
54
|
*/
|
|
@@ -116,9 +157,8 @@ const PLATFORM_CONFIG = {
|
|
|
116
157
|
audioInput: '0', // Default audio device if needed
|
|
117
158
|
audioFormat: 'avfoundation',
|
|
118
159
|
extraArgs: [
|
|
119
|
-
'-
|
|
120
|
-
'-
|
|
121
|
-
'-r', '30' // Set frame rate
|
|
160
|
+
'-capture_cursor', '1', // Capture mouse cursor
|
|
161
|
+
'-capture_mouse_clicks', '1' // Capture mouse clicks
|
|
122
162
|
]
|
|
123
163
|
},
|
|
124
164
|
win32: {
|
|
@@ -160,6 +200,7 @@ async function getPlatformArgs({ fps, includeAudio }) {
|
|
|
160
200
|
});
|
|
161
201
|
|
|
162
202
|
const args = [
|
|
203
|
+
'-thread_queue_size', '512', // Increase input buffer to prevent drops
|
|
163
204
|
'-f', config.inputFormat
|
|
164
205
|
];
|
|
165
206
|
|
|
@@ -170,12 +211,14 @@ async function getPlatformArgs({ fps, includeAudio }) {
|
|
|
170
211
|
|
|
171
212
|
args.push(
|
|
172
213
|
'-framerate', fps.toString(),
|
|
214
|
+
'-probesize', '50M', // Increase probe size for better stream detection
|
|
173
215
|
'-i', screenInput
|
|
174
216
|
);
|
|
175
217
|
|
|
176
218
|
// Add audio capture if enabled
|
|
177
219
|
if (includeAudio) {
|
|
178
220
|
args.push(
|
|
221
|
+
'-thread_queue_size', '512', // Increase audio buffer too
|
|
179
222
|
'-f', config.audioFormat,
|
|
180
223
|
'-i', config.audioInput
|
|
181
224
|
);
|
|
@@ -279,17 +322,20 @@ export async function startRecording({
|
|
|
279
322
|
const platformArgs = await getPlatformArgs({ fps, includeAudio });
|
|
280
323
|
const outputArgs = [
|
|
281
324
|
'-c:v', 'libvpx-vp9', // Use VP9 codec for better quality and compression
|
|
282
|
-
'-quality', 'realtime', // Use realtime quality preset for
|
|
283
|
-
'-cpu-used', '
|
|
284
|
-
'-deadline', 'realtime'
|
|
285
|
-
'-b:v', '2M',
|
|
286
|
-
// Remove explicit pixel format to let ffmpeg handle conversion automatically
|
|
325
|
+
'-quality', 'realtime', // Use realtime quality preset for faster encoding
|
|
326
|
+
'-cpu-used', '8', // Maximum speed (0-8, higher = faster but lower quality)
|
|
327
|
+
'-deadline', 'realtime',// Realtime encoding mode for lowest latency
|
|
328
|
+
'-b:v', '2M', // Target bitrate
|
|
287
329
|
'-r', fps.toString(), // Ensure output framerate matches input
|
|
288
|
-
'-g',
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
'-
|
|
292
|
-
'-
|
|
330
|
+
'-g', fps.toString(), // Keyframe interval = 1 second (every fps frames) - ensures frequent keyframes
|
|
331
|
+
'-force_key_frames', `expr:gte(t,n_forced*1)`, // Force keyframe every 1 second
|
|
332
|
+
// WebM options for more frequent disk writes and proper stream handling
|
|
333
|
+
'-f', 'webm', // Force WebM container format
|
|
334
|
+
'-flush_packets', '1', // Flush packets immediately to disk - critical for short recordings
|
|
335
|
+
'-fflags', '+genpts', // Generate presentation timestamps
|
|
336
|
+
'-avoid_negative_ts', 'make_zero', // Avoid negative timestamps
|
|
337
|
+
'-vsync', '1', // Ensure every frame is encoded (CFR - constant frame rate)
|
|
338
|
+
'-max_muxing_queue_size', '9999' // Large queue to prevent frame drops
|
|
293
339
|
];
|
|
294
340
|
|
|
295
341
|
if (includeAudio) {
|
|
@@ -465,29 +511,40 @@ export async function stopRecording() {
|
|
|
465
511
|
duration: recordingDuration,
|
|
466
512
|
durationSeconds: (recordingDuration / 1000).toFixed(1)
|
|
467
513
|
});
|
|
514
|
+
|
|
515
|
+
// Enforce minimum recording duration to prevent single-frame videos
|
|
516
|
+
const MIN_RECORDING_DURATION = 2000; // 2 seconds minimum
|
|
517
|
+
if (recordingDuration < MIN_RECORDING_DURATION) {
|
|
518
|
+
const waitTime = MIN_RECORDING_DURATION - recordingDuration;
|
|
519
|
+
logger.info(`Recording too short (${recordingDuration}ms), waiting ${waitTime}ms to ensure multiple frames`);
|
|
520
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
521
|
+
}
|
|
468
522
|
|
|
469
523
|
try {
|
|
470
524
|
// First try to gracefully stop FFmpeg by sending 'q'
|
|
471
525
|
if (currentRecording && currentRecording.stdin) {
|
|
472
526
|
logger.debug('Sending quit signal to FFmpeg...');
|
|
473
527
|
currentRecording.stdin.write('q');
|
|
528
|
+
currentRecording.stdin.end(); // Close stdin to signal end
|
|
474
529
|
}
|
|
475
530
|
|
|
476
|
-
// Wait for FFmpeg to finish gracefully
|
|
531
|
+
// Wait for FFmpeg to finish gracefully with realtime encoding
|
|
477
532
|
const gracefulTimeout = setTimeout(() => {
|
|
478
533
|
if (currentRecording && !currentRecording.killed) {
|
|
534
|
+
logger.warn('FFmpeg did not exit gracefully after 10s, sending SIGTERM...');
|
|
479
535
|
// If still running, try SIGTERM
|
|
480
536
|
process.kill(currentRecording.pid, 'SIGTERM');
|
|
481
537
|
}
|
|
482
|
-
},
|
|
538
|
+
}, 10000); // Wait longer for graceful shutdown
|
|
483
539
|
|
|
484
|
-
// Wait up to
|
|
540
|
+
// Wait up to 20 seconds for SIGTERM to work
|
|
485
541
|
const hardKillTimeout = setTimeout(() => {
|
|
486
542
|
if (currentRecording && !currentRecording.killed) {
|
|
543
|
+
logger.error('FFmpeg still running after SIGTERM, using SIGKILL...');
|
|
487
544
|
// If still not dead, use SIGKILL as last resort
|
|
488
545
|
process.kill(currentRecording.pid, 'SIGKILL');
|
|
489
546
|
}
|
|
490
|
-
},
|
|
547
|
+
}, 20000); // Longer timeout to avoid killing prematurely
|
|
491
548
|
|
|
492
549
|
// Wait for the process to fully exit
|
|
493
550
|
if (currentRecording) {
|
|
@@ -498,8 +555,30 @@ export async function stopRecording() {
|
|
|
498
555
|
clearTimeout(gracefulTimeout);
|
|
499
556
|
clearTimeout(hardKillTimeout);
|
|
500
557
|
|
|
501
|
-
//
|
|
502
|
-
|
|
558
|
+
// Wait for filesystem to sync and file to be written
|
|
559
|
+
// Poll for the file to exist with content
|
|
560
|
+
logger.debug('Waiting for temp file to be written...');
|
|
561
|
+
const maxWaitTime = 10000; // Wait up to 10 seconds
|
|
562
|
+
const startWait = Date.now();
|
|
563
|
+
let tempFileReady = false;
|
|
564
|
+
|
|
565
|
+
while (!tempFileReady && (Date.now() - startWait) < maxWaitTime) {
|
|
566
|
+
const tempFile = currentTempFile;
|
|
567
|
+
if (tempFile && fs.existsSync(tempFile)) {
|
|
568
|
+
const stats = fs.statSync(tempFile);
|
|
569
|
+
if (stats.size > 0) {
|
|
570
|
+
logger.debug('Temp file is ready', { size: stats.size });
|
|
571
|
+
tempFileReady = true;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Wait a bit before checking again
|
|
576
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (!tempFileReady) {
|
|
580
|
+
logger.warn('Temp file not ready after waiting, proceeding anyway');
|
|
581
|
+
}
|
|
503
582
|
|
|
504
583
|
// Read temp file path from disk (for cross-process access)
|
|
505
584
|
let tempFile = currentTempFile; // Try in-memory first
|
|
@@ -547,21 +626,41 @@ export async function stopRecording() {
|
|
|
547
626
|
path: tempFile
|
|
548
627
|
});
|
|
549
628
|
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
logger.debug('Copying temp file to final output...');
|
|
629
|
+
// Re-mux to ensure proper container metadata (duration, seekability, etc.)
|
|
630
|
+
logger.debug('Re-muxing temp file to fix container metadata...');
|
|
553
631
|
|
|
554
632
|
try {
|
|
555
|
-
|
|
556
|
-
|
|
633
|
+
// First, create a temporary fixed version
|
|
634
|
+
const fixedTempFile = tempFile.replace('.webm', '-fixed.webm');
|
|
635
|
+
|
|
636
|
+
const fixSuccess = await fixVideoContainer(tempFile, fixedTempFile);
|
|
637
|
+
|
|
638
|
+
if (fixSuccess && fs.existsSync(fixedTempFile) && fs.statSync(fixedTempFile).size > 0) {
|
|
639
|
+
// Use the fixed version
|
|
640
|
+
logger.info('Using re-muxed version with proper container metadata');
|
|
641
|
+
fs.copyFileSync(fixedTempFile, outputPath);
|
|
642
|
+
|
|
643
|
+
// Clean up the fixed temp file
|
|
644
|
+
try {
|
|
645
|
+
fs.unlinkSync(fixedTempFile);
|
|
646
|
+
} catch (e) {
|
|
647
|
+
logger.debug('Failed to delete fixed temp file:', e);
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
// Fallback: just copy the original temp file
|
|
651
|
+
logger.warn('Re-muxing failed, using original file');
|
|
652
|
+
fs.copyFileSync(tempFile, outputPath);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
logger.info('Successfully finalized recording to output');
|
|
557
656
|
|
|
558
657
|
// Verify the final file exists and has content
|
|
559
658
|
if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size === 0) {
|
|
560
|
-
throw new Error('Final output file is empty or missing after
|
|
659
|
+
throw new Error('Final output file is empty or missing after processing');
|
|
561
660
|
}
|
|
562
661
|
|
|
563
662
|
} catch (error) {
|
|
564
|
-
logger.error('Failed to
|
|
663
|
+
logger.error('Failed to process temp file:', error);
|
|
565
664
|
throw new Error('Failed to finalize recording: ' + error.message);
|
|
566
665
|
}
|
|
567
666
|
|
|
@@ -688,3 +787,8 @@ export function getRecordingStatus() {
|
|
|
688
787
|
outputPath
|
|
689
788
|
};
|
|
690
789
|
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Export the fix function for external use
|
|
793
|
+
*/
|
|
794
|
+
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
|
+
}
|
|
@@ -47,7 +47,7 @@ export class FileTracker {
|
|
|
47
47
|
fs.writeFileSync(this.trackedFile, '', 'utf8');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
this.tail = new Tail(this.trackedFile, { encoding: '
|
|
50
|
+
this.tail = new Tail(this.trackedFile, { encoding: 'utf8' });
|
|
51
51
|
this.tail.on('line', (line) => {
|
|
52
52
|
const time = Date.now();
|
|
53
53
|
this.eventTimes.push(time);
|
|
@@ -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;
|