dashcam 1.4.2-beta → 1.4.5-beta

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/lib/config.js CHANGED
@@ -26,7 +26,8 @@ export const apiEndpoints = {
26
26
  production: 'https://testdriver-api.onrender.com'
27
27
  };
28
28
 
29
- export const API_ENDPOINT = apiEndpoints[ENV];
29
+ // Allow TD_API_ROOT to override the endpoint
30
+ export const API_ENDPOINT = process.env.TD_API_ROOT || apiEndpoints[ENV];
30
31
 
31
32
  // App configuration
32
33
  export const APP = {
@@ -84,7 +84,13 @@ function filterWebEvents(
84
84
  event => event.type === 'INITIAL_TABS' || event.payload.tabId
85
85
  );
86
86
  const patterns = groupLogsStatuses
87
- .map((status) => status.items.map((item) => item.item))
87
+ .map((status) => {
88
+ // Handle cases where items might not be set (e.g., during upload)
89
+ if (!status.items || !Array.isArray(status.items)) {
90
+ return status.patterns || [];
91
+ }
92
+ return status.items.map((item) => item.item);
93
+ })
88
94
  .flat();
89
95
 
90
96
  const newEvents = [];
package/lib/logs/index.js CHANGED
@@ -123,7 +123,11 @@ async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipI
123
123
  filteredEvents
124
124
  );
125
125
  } catch (error) {
126
- logger.error('Error trimming log file', { file: status.fileLocation, error });
126
+ logger.error('Error trimming log file', {
127
+ file: status.fileLocation,
128
+ error: error.message,
129
+ stack: error.stack
130
+ });
127
131
  }
128
132
  });
129
133
 
@@ -0,0 +1,487 @@
1
+ import os from 'os';
2
+ import { logger } from './logger.js';
3
+ import pidusage from 'pidusage';
4
+ import { getTopProcesses } from './topProcesses.js';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+
8
+ /**
9
+ * Tracks CPU and memory usage during recording
10
+ */
11
+ class PerformanceTracker {
12
+ constructor() {
13
+ this.interval = null;
14
+ this.samples = [];
15
+ this.startTime = null;
16
+ this.pid = process.pid;
17
+ this.monitorInterval = 5000; // Sample every 5 seconds
18
+ this.lastNetworkStats = null; // Track previous network stats for delta calculation
19
+ this.performanceFile = null; // Path to performance data file
20
+ }
21
+
22
+ /**
23
+ * Get network I/O statistics
24
+ */
25
+ async getNetworkMetrics() {
26
+ try {
27
+ const networkInterfaces = os.networkInterfaces();
28
+
29
+ // Get network stats using os module (basic approach)
30
+ // On macOS/Linux we can read from /proc/net/dev or use system commands
31
+ let totalBytesReceived = 0;
32
+ let totalBytesSent = 0;
33
+
34
+ if (process.platform === 'darwin') {
35
+ // macOS - use netstat command
36
+ const { execSync } = await import('child_process');
37
+ try {
38
+ const output = execSync('netstat -ib', { encoding: 'utf8', timeout: 1000 });
39
+ const lines = output.split('\n');
40
+
41
+ for (const line of lines) {
42
+ const parts = line.trim().split(/\s+/);
43
+ if (parts.length >= 7 && parts[0] !== 'Name') {
44
+ const ibytes = parseInt(parts[6]) || 0;
45
+ const obytes = parseInt(parts[9]) || 0;
46
+ totalBytesReceived += ibytes;
47
+ totalBytesSent += obytes;
48
+ }
49
+ }
50
+ } catch (error) {
51
+ logger.debug('Failed to get network stats from netstat', { error: error.message });
52
+ }
53
+ } else if (process.platform === 'linux') {
54
+ // Linux - read from /proc/net/dev
55
+ const fs = await import('fs');
56
+ try {
57
+ const netDev = fs.readFileSync('/proc/net/dev', 'utf8');
58
+ const lines = netDev.split('\n');
59
+
60
+ for (const line of lines) {
61
+ if (line.includes(':')) {
62
+ const parts = line.split(':')[1].trim().split(/\s+/);
63
+ if (parts.length >= 9) {
64
+ totalBytesReceived += parseInt(parts[0]) || 0;
65
+ totalBytesSent += parseInt(parts[8]) || 0;
66
+ }
67
+ }
68
+ }
69
+ } catch (error) {
70
+ logger.debug('Failed to read /proc/net/dev', { error: error.message });
71
+ }
72
+ }
73
+
74
+ const currentStats = {
75
+ bytesReceived: totalBytesReceived,
76
+ bytesSent: totalBytesSent,
77
+ timestamp: Date.now()
78
+ };
79
+
80
+ // Calculate deltas (bytes per second)
81
+ let bytesReceivedPerSec = 0;
82
+ let bytesSentPerSec = 0;
83
+
84
+ if (this.lastNetworkStats) {
85
+ const timeDelta = (currentStats.timestamp - this.lastNetworkStats.timestamp) / 1000; // seconds
86
+ if (timeDelta > 0) {
87
+ bytesReceivedPerSec = (currentStats.bytesReceived - this.lastNetworkStats.bytesReceived) / timeDelta;
88
+ bytesSentPerSec = (currentStats.bytesSent - this.lastNetworkStats.bytesSent) / timeDelta;
89
+ }
90
+ }
91
+
92
+ this.lastNetworkStats = currentStats;
93
+
94
+ return {
95
+ network: {
96
+ bytesReceived: currentStats.bytesReceived,
97
+ bytesSent: currentStats.bytesSent,
98
+ bytesReceivedPerSec,
99
+ bytesSentPerSec,
100
+ mbReceivedPerSec: bytesReceivedPerSec / (1024 * 1024),
101
+ mbSentPerSec: bytesSentPerSec / (1024 * 1024)
102
+ }
103
+ };
104
+ } catch (error) {
105
+ logger.warn('Failed to get network metrics', { error: error.message });
106
+ return {
107
+ network: {
108
+ bytesReceived: 0,
109
+ bytesSent: 0,
110
+ bytesReceivedPerSec: 0,
111
+ bytesSentPerSec: 0,
112
+ mbReceivedPerSec: 0,
113
+ mbSentPerSec: 0
114
+ }
115
+ };
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Get current system-wide CPU and memory metrics
121
+ */
122
+ async getSystemMetrics() {
123
+ const totalMem = os.totalmem();
124
+ const freeMem = os.freemem();
125
+ const usedMem = totalMem - freeMem;
126
+
127
+ // Get CPU info
128
+ const cpus = os.cpus();
129
+
130
+ // Calculate average CPU usage across all cores
131
+ let totalIdle = 0;
132
+ let totalTick = 0;
133
+
134
+ cpus.forEach(cpu => {
135
+ for (const type in cpu.times) {
136
+ totalTick += cpu.times[type];
137
+ }
138
+ totalIdle += cpu.times.idle;
139
+ });
140
+
141
+ return {
142
+ system: {
143
+ totalMemory: totalMem,
144
+ freeMemory: freeMem,
145
+ usedMemory: usedMem,
146
+ memoryUsagePercent: (usedMem / totalMem) * 100,
147
+ cpuCount: cpus.length,
148
+ totalIdle,
149
+ totalTick
150
+ }
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Get process-specific CPU and memory metrics
156
+ */
157
+ async getProcessMetrics() {
158
+ try {
159
+ const stats = await pidusage(this.pid);
160
+ return {
161
+ process: {
162
+ cpu: stats.cpu, // CPU usage percentage
163
+ memory: stats.memory, // Memory in bytes
164
+ ppid: stats.ppid,
165
+ pid: stats.pid,
166
+ ctime: stats.ctime,
167
+ elapsed: stats.elapsed,
168
+ timestamp: stats.timestamp
169
+ }
170
+ };
171
+ } catch (error) {
172
+ logger.warn('Failed to get process metrics', { error: error.message });
173
+ return {
174
+ process: {
175
+ cpu: 0,
176
+ memory: 0,
177
+ pid: this.pid
178
+ }
179
+ };
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Get top processes by CPU and memory usage
185
+ */
186
+ async getTopProcessesData() {
187
+ try {
188
+ // Get top 10 processes using cross-platform implementation
189
+ const topProcs = await getTopProcesses(10);
190
+
191
+ if (!topProcs || topProcs.length === 0) {
192
+ return {
193
+ topProcesses: [],
194
+ totalProcesses: 0
195
+ };
196
+ }
197
+
198
+ // Get detailed stats using pidusage for each process
199
+ const detailedStats = [];
200
+ for (const proc of topProcs) {
201
+ try {
202
+ const stats = await pidusage(proc.pid);
203
+ detailedStats.push({
204
+ pid: proc.pid,
205
+ name: proc.name,
206
+ cpu: stats.cpu,
207
+ memory: stats.memory,
208
+ ppid: stats.ppid,
209
+ ctime: stats.ctime,
210
+ elapsed: stats.elapsed
211
+ });
212
+ } catch (error) {
213
+ // Process might have exited, use basic data from ps/PowerShell
214
+ logger.debug('Failed to get detailed stats for process, using basic data', {
215
+ pid: proc.pid,
216
+ error: error.message
217
+ });
218
+ detailedStats.push({
219
+ pid: proc.pid,
220
+ name: proc.name,
221
+ cpu: proc.cpu || 0,
222
+ memory: proc.memBytes || (proc.mem || 0) * 1024 * 1024, // Convert % to rough bytes or use WS
223
+ ppid: 0,
224
+ ctime: 0,
225
+ elapsed: 0
226
+ });
227
+ }
228
+ }
229
+
230
+ // Already sorted by CPU from getTopProcesses
231
+ return {
232
+ topProcesses: detailedStats,
233
+ totalProcesses: topProcs.length
234
+ };
235
+ } catch (error) {
236
+ logger.warn('Failed to get top processes', { error: error.message });
237
+ return {
238
+ topProcesses: [],
239
+ totalProcesses: 0
240
+ };
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Collect a performance sample (with top processes)
246
+ */
247
+ async collectSample() {
248
+ const timestamp = Date.now();
249
+ const elapsedMs = this.startTime ? timestamp - this.startTime : 0;
250
+
251
+ try {
252
+ // Collect all metrics including top processes
253
+ const [systemMetrics, processMetrics, networkMetrics, topProcessesData] = await Promise.all([
254
+ this.getSystemMetrics(),
255
+ this.getProcessMetrics(),
256
+ this.getNetworkMetrics(),
257
+ this.getTopProcessesData()
258
+ ]);
259
+
260
+ const sample = {
261
+ timestamp,
262
+ elapsedMs,
263
+ ...systemMetrics,
264
+ ...processMetrics,
265
+ ...networkMetrics,
266
+ ...topProcessesData
267
+ };
268
+
269
+ this.samples.push(sample);
270
+
271
+ // Save sample to file immediately
272
+ if (this.performanceFile) {
273
+ try {
274
+ fs.appendFileSync(this.performanceFile, JSON.stringify(sample) + '\n');
275
+ } catch (error) {
276
+ logger.warn('Failed to write performance sample to file', { error: error.message });
277
+ }
278
+ }
279
+
280
+ // Log sample in verbose mode
281
+ logger.verbose('Performance sample collected', {
282
+ elapsedSeconds: (elapsedMs / 1000).toFixed(1),
283
+ systemMemoryUsage: `${sample.system.memoryUsagePercent.toFixed(1)}%`,
284
+ processMemoryMB: (sample.process.memory / (1024 * 1024)).toFixed(1),
285
+ processCPU: `${sample.process.cpu.toFixed(1)}%`,
286
+ networkIn: `${sample.network.mbReceivedPerSec.toFixed(2)} MB/s`,
287
+ networkOut: `${sample.network.mbSentPerSec.toFixed(2)} MB/s`,
288
+ topProcessesCount: sample.topProcesses?.length || 0
289
+ });
290
+
291
+ } catch (error) {
292
+ logger.warn('Failed to collect performance sample', { error: error.message });
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Start tracking performance - lightweight version with system metrics only
298
+ */
299
+ start(outputDir = null) {
300
+ if (this.interval) {
301
+ logger.warn('Performance tracking already started');
302
+ return;
303
+ }
304
+
305
+ this.startTime = Date.now();
306
+ this.samples = [];
307
+
308
+ // Set up performance data file
309
+ if (outputDir) {
310
+ this.performanceFile = path.join(outputDir, 'performance.jsonl');
311
+ // Clear any existing file
312
+ try {
313
+ if (fs.existsSync(this.performanceFile)) {
314
+ fs.unlinkSync(this.performanceFile);
315
+ }
316
+ } catch (error) {
317
+ logger.warn('Failed to clear performance file', { error: error.message });
318
+ }
319
+ }
320
+
321
+ logger.info('Starting performance tracking (with top processes)', {
322
+ pid: this.pid,
323
+ monitorInterval: this.monitorInterval,
324
+ performanceFile: this.performanceFile
325
+ });
326
+
327
+ // Collect initial sample
328
+ this.collectSample();
329
+
330
+ // Start periodic collection every 5 seconds
331
+ this.interval = setInterval(() => {
332
+ this.collectSample();
333
+ }, this.monitorInterval);
334
+ }
335
+
336
+ /**
337
+ * Stop tracking and return summary
338
+ */
339
+ stop() {
340
+ if (this.interval) {
341
+ clearInterval(this.interval);
342
+ this.interval = null;
343
+ }
344
+
345
+ // If we have a performance file, read all samples from it
346
+ if (this.performanceFile && fs.existsSync(this.performanceFile)) {
347
+ try {
348
+ const fileContent = fs.readFileSync(this.performanceFile, 'utf8');
349
+ const lines = fileContent.trim().split('\n').filter(line => line.length > 0);
350
+ this.samples = lines.map(line => JSON.parse(line));
351
+ logger.info('Loaded performance samples from file', {
352
+ sampleCount: this.samples.length,
353
+ file: this.performanceFile
354
+ });
355
+ } catch (error) {
356
+ logger.warn('Failed to read performance samples from file', { error: error.message });
357
+ }
358
+ }
359
+
360
+ if (this.samples.length === 0) {
361
+ logger.warn('No performance samples collected');
362
+ return {
363
+ samples: [],
364
+ summary: null
365
+ };
366
+ }
367
+
368
+ // Calculate summary statistics
369
+ const summary = this.calculateSummary();
370
+
371
+ logger.info('Performance tracking stopped', {
372
+ totalSamples: this.samples.length,
373
+ duration: summary.durationMs,
374
+ avgProcessCPU: `${summary.avgProcessCPU.toFixed(1)}%`,
375
+ avgProcessMemoryMB: summary.avgProcessMemoryMB.toFixed(1),
376
+ maxProcessCPU: `${summary.maxProcessCPU.toFixed(1)}%`,
377
+ maxProcessMemoryMB: summary.maxProcessMemoryMB.toFixed(1)
378
+ });
379
+
380
+ const result = {
381
+ samples: this.samples,
382
+ summary
383
+ };
384
+
385
+ // DON'T delete the performance file - keep it for the stop command to read
386
+ // The stop command or uploader will clean it up after reading
387
+ logger.debug('Keeping performance file for upload', { file: this.performanceFile });
388
+
389
+ // Reset in-memory state but keep file path for cleanup later
390
+ this.samples = [];
391
+ this.startTime = null;
392
+ // Don't reset this.performanceFile - caller may need it
393
+
394
+ return result;
395
+ }
396
+
397
+ /**
398
+ * Calculate summary statistics from samples
399
+ */
400
+ calculateSummary() {
401
+ if (this.samples.length === 0) {
402
+ return null;
403
+ }
404
+
405
+ const firstSample = this.samples[0];
406
+ const lastSample = this.samples[this.samples.length - 1];
407
+
408
+ // Calculate averages and max values
409
+ let totalProcessCPU = 0;
410
+ let totalProcessMemory = 0;
411
+ let totalSystemMemoryUsage = 0;
412
+ let maxProcessCPU = 0;
413
+ let maxProcessMemory = 0;
414
+ let maxSystemMemoryUsage = 0;
415
+
416
+ this.samples.forEach(sample => {
417
+ const processCPU = sample.process.cpu || 0;
418
+ const processMemory = sample.process.memory || 0;
419
+ const systemMemoryUsage = sample.system.memoryUsagePercent || 0;
420
+
421
+ totalProcessCPU += processCPU;
422
+ totalProcessMemory += processMemory;
423
+ totalSystemMemoryUsage += systemMemoryUsage;
424
+
425
+ maxProcessCPU = Math.max(maxProcessCPU, processCPU);
426
+ maxProcessMemory = Math.max(maxProcessMemory, processMemory);
427
+ maxSystemMemoryUsage = Math.max(maxSystemMemoryUsage, systemMemoryUsage);
428
+ });
429
+
430
+ const count = this.samples.length;
431
+
432
+ // Calculate network totals from last sample
433
+ const finalSample = this.samples[this.samples.length - 1];
434
+ const totalBytesReceived = finalSample.network?.bytesReceived || 0;
435
+ const totalBytesSent = finalSample.network?.bytesSent || 0;
436
+
437
+ return {
438
+ durationMs: lastSample.timestamp - firstSample.timestamp,
439
+ sampleCount: count,
440
+ monitorInterval: this.monitorInterval,
441
+ // Process metrics
442
+ avgProcessCPU: totalProcessCPU / count,
443
+ maxProcessCPU,
444
+ avgProcessMemoryBytes: totalProcessMemory / count,
445
+ avgProcessMemoryMB: (totalProcessMemory / count) / (1024 * 1024),
446
+ maxProcessMemoryBytes: maxProcessMemory,
447
+ maxProcessMemoryMB: maxProcessMemory / (1024 * 1024),
448
+ // System metrics
449
+ avgSystemMemoryUsagePercent: totalSystemMemoryUsage / count,
450
+ maxSystemMemoryUsagePercent: maxSystemMemoryUsage,
451
+ totalSystemMemoryBytes: firstSample.system.totalMemory,
452
+ totalSystemMemoryGB: firstSample.system.totalMemory / (1024 * 1024 * 1024),
453
+ // Network metrics
454
+ totalBytesReceived,
455
+ totalBytesSent,
456
+ totalMBReceived: totalBytesReceived / (1024 * 1024),
457
+ totalMBSent: totalBytesSent / (1024 * 1024)
458
+ };
459
+ }
460
+
461
+ /**
462
+ * Check if tracking is active
463
+ */
464
+ isTracking() {
465
+ return this.interval !== null;
466
+ }
467
+
468
+ /**
469
+ * Cleanup performance file (call after upload)
470
+ */
471
+ cleanup() {
472
+ if (this.performanceFile && fs.existsSync(this.performanceFile)) {
473
+ try {
474
+ fs.unlinkSync(this.performanceFile);
475
+ logger.debug('Cleaned up performance file', { file: this.performanceFile });
476
+ } catch (error) {
477
+ logger.warn('Failed to cleanup performance file', { error: error.message });
478
+ }
479
+ }
480
+ this.performanceFile = null;
481
+ }
482
+ }
483
+
484
+ // Create singleton instance
485
+ const performanceTracker = new PerformanceTracker();
486
+
487
+ export { performanceTracker, PerformanceTracker };
@@ -283,31 +283,50 @@ class ProcessManager {
283
283
 
284
284
  logger.info('Killing background process', { pid });
285
285
 
286
- // Kill the process immediately - no graceful shutdown needed
287
- // Video is already being streamed to disk, we'll fix it with FFmpeg
286
+ // Try graceful shutdown first with SIGTERM (allows cleanup handlers to run)
287
+ // Then use SIGKILL if process doesn't exit
288
288
  try {
289
- process.kill(pid, 'SIGKILL');
290
- logger.info('Sent SIGKILL to background process');
289
+ process.kill(pid, 'SIGTERM');
290
+ logger.info('Sent SIGTERM to background process');
291
291
  } catch (error) {
292
- logger.error('Failed to kill background process', { error });
292
+ logger.error('Failed to send SIGTERM to background process', { error });
293
293
  throw new Error('Failed to stop background recording process');
294
294
  }
295
295
 
296
- // Wait briefly for process to die
297
- logger.debug('Waiting for background process to exit...');
298
- const maxWaitTime = 5000; // 5 seconds should be plenty for SIGKILL
296
+ // Wait for graceful shutdown
297
+ logger.debug('Waiting for background process to exit gracefully...');
298
+ const maxGracefulWait = 3000; // 3 seconds for graceful shutdown
299
299
  const startWait = Date.now();
300
300
 
301
- while (this.isProcessRunning(pid) && (Date.now() - startWait) < maxWaitTime) {
301
+ while (this.isProcessRunning(pid) && (Date.now() - startWait) < maxGracefulWait) {
302
302
  await new Promise(resolve => setTimeout(resolve, 100));
303
303
  }
304
304
 
305
+ // If still running, force kill with SIGKILL
305
306
  if (this.isProcessRunning(pid)) {
306
- logger.error('Background process did not exit after SIGKILL');
307
- throw new Error('Failed to kill background process');
307
+ logger.warn('Background process did not exit gracefully, sending SIGKILL');
308
+ try {
309
+ process.kill(pid, 'SIGKILL');
310
+ logger.info('Sent SIGKILL to background process');
311
+
312
+ // Wait for SIGKILL to take effect
313
+ const maxKillWait = 2000; // 2 seconds should be plenty for SIGKILL
314
+ const killStart = Date.now();
315
+ while (this.isProcessRunning(pid) && (Date.now() - killStart) < maxKillWait) {
316
+ await new Promise(resolve => setTimeout(resolve, 100));
317
+ }
318
+
319
+ if (this.isProcessRunning(pid)) {
320
+ logger.error('Background process did not exit even after SIGKILL');
321
+ throw new Error('Failed to kill background process');
322
+ }
323
+ } catch (error) {
324
+ logger.error('Failed to SIGKILL background process', { error });
325
+ throw new Error('Failed to force kill background process');
326
+ }
308
327
  }
309
328
 
310
- logger.info('Background process killed');
329
+ logger.info('Background process stopped');
311
330
 
312
331
  // Mark status as completed
313
332
  this.markStatusCompleted({