dashcam 1.4.2-beta → 1.4.3-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.
@@ -161,11 +161,39 @@ async function runBackgroundRecording() {
161
161
  }
162
162
  isShuttingDown = true;
163
163
 
164
- logger.info(`Received ${signal}, background process will be killed`);
165
- console.log('[Background] Received stop signal, process will be terminated...');
164
+ logger.info(`Received ${signal}, cleaning up child processes`);
165
+ console.log('[Background] Received stop signal, cleaning up...');
166
+
167
+ // Kill any child processes (ffmpeg, etc.)
168
+ try {
169
+ // Get all child processes and kill them
170
+ const { exec } = await import('child_process');
171
+ const platform = process.platform;
172
+
173
+ if (platform === 'darwin' || platform === 'linux') {
174
+ // On Unix, kill the entire process group
175
+ exec(`pkill -P ${process.pid}`, (error) => {
176
+ if (error && error.code !== 1) { // code 1 means no processes found
177
+ logger.warn('Failed to kill child processes', { error: error.message });
178
+ }
179
+ logger.info('Child processes killed');
180
+ });
181
+ } else if (platform === 'win32') {
182
+ // On Windows, use taskkill
183
+ exec(`taskkill /F /T /PID ${process.pid}`, (error) => {
184
+ if (error) {
185
+ logger.warn('Failed to kill child processes on Windows', { error: error.message });
186
+ }
187
+ logger.info('Child processes killed');
188
+ });
189
+ }
190
+
191
+ // Give it a moment to clean up
192
+ await new Promise(resolve => setTimeout(resolve, 500));
193
+ } catch (error) {
194
+ logger.error('Error during cleanup', { error: error.message });
195
+ }
166
196
 
167
- // Don't try to stop recording here - the main process will handle cleanup
168
- // after killing this process. Just exit.
169
197
  logger.info('Background process exiting');
170
198
  process.exit(0);
171
199
  };
package/bin/dashcam.js CHANGED
@@ -622,14 +622,33 @@ program
622
622
  const { jsonl } = await import('../lib/utilities/jsonl.js');
623
623
  const webLogs = jsonl.read(webLogsFile);
624
624
  if (webLogs && webLogs.length > 0) {
625
+ // Load web config to get patterns
626
+ const webConfigFile = path.join(process.cwd(), '.dashcam', 'web-config.json');
627
+ let webPatterns = ['*']; // Default to all patterns
628
+ if (fs.existsSync(webConfigFile)) {
629
+ try {
630
+ const webConfig = JSON.parse(fs.readFileSync(webConfigFile, 'utf8'));
631
+ // Extract all patterns from all enabled configs
632
+ webPatterns = Object.values(webConfig)
633
+ .filter(config => config.enabled !== false)
634
+ .flatMap(config => config.patterns || []);
635
+ if (webPatterns.length === 0) {
636
+ webPatterns = ['*'];
637
+ }
638
+ } catch (error) {
639
+ logger.warn('Failed to load web config for patterns', { error: error.message });
640
+ }
641
+ }
642
+
625
643
  logTrackingResults.push({
626
644
  type: 'web',
627
645
  name: 'Web Logs',
628
646
  fileLocation: webLogsFile,
629
647
  count: webLogs.length,
630
- trimmedFileLocation: webLogsFile
648
+ trimmedFileLocation: webLogsFile,
649
+ patterns: webPatterns // Include patterns for filtering
631
650
  });
632
- logger.info('Found web logs', { count: webLogs.length, file: webLogsFile });
651
+ logger.info('Found web logs', { count: webLogs.length, file: webLogsFile, patterns: webPatterns });
633
652
  }
634
653
  }
635
654
 
@@ -719,6 +738,7 @@ program
719
738
  .option('--remove <id>', 'Remove a log tracker by ID')
720
739
  .option('--list', 'List all configured log trackers')
721
740
  .option('--status', 'Show log tracking status')
741
+ .option('--view [directory]', 'View logs from recording directory (defaults to most recent in /tmp/dashcam/recordings)')
722
742
  .option('--name <name>', 'Name for the log tracker (required with --add)')
723
743
  .option('--type <type>', 'Type of tracker: "web" or "file" (required with --add)')
724
744
  .option('--pattern <pattern>', 'Pattern to track (can be used multiple times)', (value, previous) => {
@@ -812,6 +832,16 @@ program
812
832
  console.log(` File trackers: ${status.cliFilesCount}`);
813
833
  console.log(` Web trackers: ${status.webAppsCount}`);
814
834
  console.log(` Total recent events: ${status.totalEvents}`);
835
+ console.log(` Web daemon running: ${status.webDaemonRunning ? 'Yes' : 'No'}`);
836
+
837
+ // Show WebSocket server info
838
+ const { server } = await import('../lib/websocket/server.js');
839
+ if (server.isListening.value) {
840
+ console.log(` WebSocket server: Listening on port ${server.port}`);
841
+ console.log(` WebSocket clients: ${server._socket?.clients?.size || 0} connected`);
842
+ } else {
843
+ console.log(` WebSocket server: Not listening`);
844
+ }
815
845
 
816
846
  if (status.fileTrackerStats.length > 0) {
817
847
  console.log('\n File tracker activity (last minute):');
@@ -819,13 +849,127 @@ program
819
849
  console.log(` ${stat.filePath}: ${stat.count} events`);
820
850
  });
821
851
  }
852
+
853
+ if (status.webApps.length > 0) {
854
+ console.log('\n Web tracker patterns:');
855
+ status.webApps.forEach(app => {
856
+ console.log(` ${app.name}: ${app.patterns.join(', ')}`);
857
+ });
858
+ }
859
+ } else if (options.view !== undefined) {
860
+ // View logs from a recording directory
861
+ const { jsonl } = await import('../lib/utilities/jsonl.js');
862
+
863
+ let targetDir = options.view;
864
+
865
+ // If no directory specified, find the most recent recording directory
866
+ if (!targetDir || targetDir === true) {
867
+ const recordingsDir = path.join(os.tmpdir(), 'dashcam', 'recordings');
868
+ if (!fs.existsSync(recordingsDir)) {
869
+ console.error('No recordings directory found at:', recordingsDir);
870
+ process.exit(1);
871
+ }
872
+
873
+ const entries = fs.readdirSync(recordingsDir, { withFileTypes: true });
874
+ const dirs = entries
875
+ .filter(entry => entry.isDirectory())
876
+ .map(entry => ({
877
+ name: entry.name,
878
+ path: path.join(recordingsDir, entry.name),
879
+ mtime: fs.statSync(path.join(recordingsDir, entry.name)).mtime
880
+ }))
881
+ .sort((a, b) => b.mtime - a.mtime);
882
+
883
+ if (dirs.length === 0) {
884
+ console.error('No recording directories found in:', recordingsDir);
885
+ process.exit(1);
886
+ }
887
+
888
+ targetDir = dirs[0].path;
889
+ console.log('Viewing logs from most recent recording:', path.basename(targetDir));
890
+ } else if (!fs.existsSync(targetDir)) {
891
+ console.error('Directory does not exist:', targetDir);
892
+ process.exit(1);
893
+ }
894
+
895
+ console.log('Directory:', targetDir);
896
+ console.log('');
897
+
898
+ // Check for CLI logs
899
+ const cliLogsFile = path.join(targetDir, 'dashcam_logs_cli.jsonl');
900
+ if (fs.existsSync(cliLogsFile)) {
901
+ const cliLogs = jsonl.read(cliLogsFile);
902
+ if (cliLogs && cliLogs.length > 0) {
903
+ console.log(`📄 CLI Logs (${cliLogs.length} events):`);
904
+ cliLogs.slice(0, 50).forEach((log, index) => {
905
+ const timeSeconds = ((log.time || 0) / 1000).toFixed(2);
906
+ const logFile = log.logFile || 'unknown';
907
+ const content = (log.line || log.content || '').substring(0, 100);
908
+ console.log(` [${timeSeconds}s] ${logFile}: ${content}`);
909
+ });
910
+ if (cliLogs.length > 50) {
911
+ console.log(` ... and ${cliLogs.length - 50} more events`);
912
+ }
913
+ console.log('');
914
+ }
915
+ } else {
916
+ console.log('📄 CLI Logs: No logs found');
917
+ console.log('');
918
+ }
919
+
920
+ // Check for web logs
921
+ const webLogsFile = path.join(targetDir, 'dashcam_logs_web_events.jsonl');
922
+ if (fs.existsSync(webLogsFile)) {
923
+ const webLogs = jsonl.read(webLogsFile);
924
+ if (webLogs && webLogs.length > 0) {
925
+ console.log(`🌐 Web Logs (${webLogs.length} events):`);
926
+
927
+ // Group by event type
928
+ const eventTypes = {};
929
+ webLogs.forEach(log => {
930
+ eventTypes[log.type] = (eventTypes[log.type] || 0) + 1;
931
+ });
932
+
933
+ console.log(' Event types:');
934
+ Object.entries(eventTypes).forEach(([type, count]) => {
935
+ console.log(` ${type}: ${count}`);
936
+ });
937
+ console.log('');
938
+
939
+ // Show first 20 events
940
+ console.log(' Recent events:');
941
+ webLogs.slice(0, 20).forEach((log, index) => {
942
+ const timeSeconds = ((log.time || 0) / 1000).toFixed(2);
943
+ const type = log.type || 'unknown';
944
+
945
+ if (type === 'LOG_EVENT' || type === 'LOG_ERROR') {
946
+ const message = log.payload?.message || '';
947
+ console.log(` [${timeSeconds}s] ${type}: ${message.substring(0, 80)}`);
948
+ } else if (type.startsWith('NETWORK_')) {
949
+ const url = log.payload?.url || '';
950
+ console.log(` [${timeSeconds}s] ${type}: ${url.substring(0, 80)}`);
951
+ } else {
952
+ console.log(` [${timeSeconds}s] ${type}`);
953
+ }
954
+ });
955
+ if (webLogs.length > 20) {
956
+ console.log(` ... and ${webLogs.length - 20} more events`);
957
+ }
958
+ console.log('');
959
+ }
960
+ } else {
961
+ console.log('🌐 Web Logs: No logs found');
962
+ console.log('');
963
+ }
822
964
  } else {
823
- console.log('Please specify an action: --add, --remove, --list, or --status');
965
+ console.log('Please specify an action: --add, --remove, --list, --status, or --view');
824
966
  console.log('\nExamples:');
825
967
  console.log(' dashcam logs --add --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
826
968
  console.log(' dashcam logs --add --name=app-logs --type=file --file=/var/log/app.log');
827
969
  console.log(' dashcam logs --list');
828
970
  console.log(' dashcam logs --status');
971
+ console.log(' dashcam logs --view # View logs from most recent recording');
972
+ console.log(' dashcam logs --view /path/to/recording # View logs from specific directory');
829
973
  console.log('\nUse "dashcam logs --help" for more information');
830
974
  }
831
975
 
@@ -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
 
@@ -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({
package/lib/recorder.js CHANGED
@@ -414,19 +414,12 @@ export async function startRecording({
414
414
  reject: false,
415
415
  all: true, // Capture both stdout and stderr
416
416
  stdin: 'pipe', // Enable stdin for sending 'q' to stop recording
417
- detached: false,
417
+ detached: false, // Keep attached so it dies with parent
418
418
  windowsHide: true // Hide the console window on Windows
419
419
  });
420
420
 
421
- // Unref the child process so it doesn't keep the parent alive
422
- if (currentRecording.pid) {
423
- try {
424
- currentRecording.unref();
425
- } catch (e) {
426
- // Ignore errors if unref is not available
427
- logger.debug('Could not unref ffmpeg process');
428
- }
429
- }
421
+ // Don't unref - we want FFmpeg to be killed when parent dies
422
+ // Removing unref() ensures orphaned processes don't hang around
430
423
 
431
424
  logger.info('FFmpeg process spawned', {
432
425
  pid: currentRecording.pid,
@@ -62,14 +62,23 @@ export const jsonl = {
62
62
  let fd = fs.openSync(file, 'w');
63
63
  fs.closeSync(fd);
64
64
  } catch (error) {
65
- throttledLog('info', `jsonl.js failed to initialize file ${error}`);
65
+ throttledLog('info', `jsonl.js failed to initialize file ${error.message}`, {
66
+ directory,
67
+ fileName,
68
+ error: error.message
69
+ });
70
+ throw error;
66
71
  }
67
72
  }
68
73
  try {
69
74
  let data = arrayOfJsonObjects.map((x) => JSON.stringify(x)).join('\n');
70
75
  fs.writeFileSync(file, data);
71
76
  } catch (error) {
72
- throttledLog('info', `jsonl.js failed to write to file ${error}`);
77
+ throttledLog('info', `jsonl.js failed to write to file ${error.message}`, {
78
+ file,
79
+ error: error.message
80
+ });
81
+ throw error;
73
82
  }
74
83
 
75
84
  return file;
@@ -51,7 +51,7 @@ class WSServer {
51
51
  return;
52
52
  }
53
53
 
54
- logger.debug('WebSocketServer: Starting server, trying ports...', { ports: this.ports });
54
+ logger.info('WebSocketServer: Starting server, trying ports...', { ports: this.ports });
55
55
  for (const port of this.ports) {
56
56
  const ws = await new Promise((resolve) => {
57
57
  logger.debug('WebSocketServer: Trying port ' + port);
@@ -104,7 +104,11 @@ class WSServer {
104
104
  });
105
105
 
106
106
  this.#socket.on('connection', (client) => {
107
- logger.info('WebSocketServer: New client connection established');
107
+ logger.info('WebSocketServer: New client connection established', {
108
+ clientAddress: client._socket?.remoteAddress,
109
+ clientPort: client._socket?.remotePort,
110
+ totalClients: this.#socket.clients.size
111
+ });
108
112
 
109
113
  let state = states.NEW;
110
114
  const failValidation = () => {
@@ -124,17 +128,22 @@ class WSServer {
124
128
 
125
129
  client.on('message', (data, isBinary) => {
126
130
  let message = isBinary ? data : data.toString();
127
- logger.debug('WebSocketServer: Received message from client', {
131
+ logger.info('WebSocketServer: Received message from client', {
128
132
  isBinary,
129
133
  messageLength: message.length,
134
+ messagePreview: message.substring(0, 100),
130
135
  state
131
136
  });
132
137
 
133
138
  try {
134
139
  message = JSON.parse(message);
135
- logger.debug('WebSocketServer: Parsed message', { type: message.type, hasPayload: !!message.payload });
140
+ logger.info('WebSocketServer: Parsed message', {
141
+ type: message.type,
142
+ hasPayload: !!message.payload,
143
+ payloadKeys: message.payload ? Object.keys(message.payload) : []
144
+ });
136
145
  } catch (err) {
137
- logger.debug('WebSocketServer: Message is not JSON, treating as raw string');
146
+ logger.info('WebSocketServer: Message is not JSON, treating as raw string', { rawMessage: message });
138
147
  }
139
148
 
140
149
  if (state === states.SENT_HEADERS) {
@@ -152,14 +161,14 @@ class WSServer {
152
161
  this.emit('message', message, client);
153
162
  });
154
163
 
155
- logger.debug('WebSocketServer: Sending connection header to client');
164
+ logger.info('WebSocketServer: Sending connection header to client');
156
165
  client.send('dashcam_desktop_socket_connected', (err) => {
157
166
  if (err) {
158
167
  logger.error('WebSocketServer: Failed to send connection header', { error: err.message });
159
168
  client.close();
160
169
  clearTimeout(timeout);
161
170
  } else {
162
- logger.debug('WebSocketServer: Connection header sent, waiting for confirmation');
171
+ logger.info('WebSocketServer: Connection header sent, waiting for confirmation');
163
172
  state = states.SENT_HEADERS;
164
173
  }
165
174
  });
@@ -176,9 +185,10 @@ class WSServer {
176
185
  throw new Error('Server not currently running');
177
186
  }
178
187
 
179
- logger.debug('WebSocketServer: Broadcasting message to all clients', {
188
+ logger.info('WebSocketServer: Broadcasting message to all clients', {
180
189
  clientCount: this.#socket.clients.size,
181
- messageType: message.type || 'raw'
190
+ messageType: message.type || 'raw',
191
+ messagePayload: message.payload ? JSON.stringify(message.payload).substring(0, 100) : 'none'
182
192
  });
183
193
 
184
194
  this.#socket.clients.forEach((client) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dashcam",
3
- "version": "1.4.2-beta",
3
+ "version": "1.4.3-beta",
4
4
  "description": "Minimal CLI version of Dashcam desktop app",
5
5
  "main": "bin/dashcam.js",
6
6
  "bin": {
package/test_workflow.sh CHANGED
@@ -21,11 +21,13 @@ echo "✅ Web tracking configured"
21
21
  echo ""
22
22
  echo "3. Setting up file tracking..."
23
23
  TEMP_FILE="/tmp/test-cli-log.txt"
24
- echo "Using existing test file: $TEMP_FILE"
25
24
 
26
- # File is already tracked from previous tests, check if it exists
27
- if [ ! -f "$TEMP_FILE" ]; then
28
- touch "$TEMP_FILE"
25
+ # Clear the file to start fresh (remove old events from previous test runs)
26
+ > "$TEMP_FILE"
27
+ echo "Created fresh test file: $TEMP_FILE"
28
+
29
+ # File is already tracked from previous tests, check if it exists in config
30
+ if ! ./bin/dashcam.js logs --list 2>/dev/null | grep -q "$TEMP_FILE"; then
29
31
  ./bin/dashcam.js logs --add --name=temp-file-tracking --type=file --file="$TEMP_FILE"
30
32
  fi
31
33
  echo "✅ File tracking configured"