dashcam 1.3.4-beta → 1.3.6-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/bin/dashcam.js CHANGED
@@ -109,7 +109,7 @@ async function recordingAction(options, command) {
109
109
  }, 2000))
110
110
  ]);
111
111
  if (!hasPermissions) {
112
- log('\n⚠️ Cannot start recording without screen recording permission.');
112
+ log('\nCannot start recording without screen recording permission.');
113
113
  process.exit(1);
114
114
  }
115
115
 
@@ -134,13 +134,17 @@ async function recordingAction(options, command) {
134
134
 
135
135
  const result = await Promise.race([startRecordingPromise, timeoutPromise]);
136
136
 
137
- log(`✅ Recording started successfully (PID: ${result.pid})`);
137
+ log(`Recording started successfully (PID: ${result.pid})`);
138
138
  log(`Output: ${result.outputPath}`);
139
139
  log('');
140
140
  log('Use "dashcam status" to check progress');
141
141
  log('Use "dashcam stop" to stop recording and upload');
142
142
 
143
- process.exit(0);
143
+ // Force immediate exit - don't wait for event loop to drain
144
+ // This is necessary when called from scripts/automation
145
+ setImmediate(() => {
146
+ process.exit(0);
147
+ });
144
148
  } catch (error) {
145
149
  logError('Failed to start recording:', error.message);
146
150
  process.exit(1);
@@ -413,7 +417,7 @@ program
413
417
 
414
418
  // Wait for upload to complete (background process handles this)
415
419
  logger.debug('Waiting for background upload to complete...');
416
- console.log('Uploading recording...');
420
+ console.log('Uploading recording...');
417
421
 
418
422
  // Wait up to 2 minutes for upload result to appear
419
423
  const maxWaitForUpload = 120000; // 2 minutes
@@ -430,7 +434,7 @@ program
430
434
  logger.debug('Upload result read attempt', { found: !!uploadResult, shareLink: uploadResult?.shareLink });
431
435
 
432
436
  if (uploadResult && uploadResult.shareLink) {
433
- console.log('📹 Watch your recording:', uploadResult.shareLink);
437
+ console.log('Watch your recording:', uploadResult.shareLink);
434
438
  // Clean up the result file now that we've read it
435
439
  processManager.cleanup();
436
440
  process.exit(0);
@@ -442,7 +446,7 @@ program
442
446
  (!result.snapshotPath || fs.existsSync(result.snapshotPath));
443
447
 
444
448
  if (!filesExist) {
445
- console.log('Recording uploaded by background process');
449
+ console.log('Recording uploaded by background process');
446
450
  logger.info('Files were cleaned up by background process');
447
451
  process.exit(0);
448
452
  }
@@ -462,7 +466,7 @@ program
462
466
  snapshotPath: result.snapshotPath
463
467
  });
464
468
 
465
- console.log('📹 Watch your recording:', uploadResult.shareLink);
469
+ console.log('Watch your recording:', uploadResult.shareLink);
466
470
  } catch (uploadError) {
467
471
  console.error('Upload failed:', uploadError.message);
468
472
  console.log('Recording saved locally:', result.outputPath);
@@ -677,7 +681,7 @@ program
677
681
  project: options.project
678
682
  });
679
683
 
680
- console.log(' Upload complete! Share link:', uploadResult.shareLink);
684
+ console.log('> Upload complete! Share link:', uploadResult.shareLink);
681
685
  process.exit(0);
682
686
 
683
687
  } catch (error) {
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
4
  import { fileURLToPath } from 'url';
5
+ import { spawn } from 'child_process';
5
6
  import { logger } from './logger.js';
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url);
@@ -148,35 +149,57 @@ class ProcessManager {
148
149
 
149
150
  try {
150
151
  const status = this.readStatus();
152
+ const pid = this.readPid();
151
153
 
152
154
  if (!status || !status.isRecording) {
153
155
  logger.warn('No active recording found');
154
156
  return false;
155
157
  }
156
158
 
157
- // Import recorder module
158
- const { stopRecording } = await import('./recorder.js');
159
+ if (!pid || !this.isProcessRunning(pid)) {
160
+ logger.warn('Background process not running');
161
+ this.cleanup({ preserveResult: true });
162
+ return false;
163
+ }
159
164
 
160
- logger.info('Stopping recording directly');
165
+ logger.info('Sending stop signal to background process', { pid });
161
166
 
162
- // Stop the recording
163
- const result = await stopRecording();
167
+ // Send SIGTERM to the background process to trigger graceful shutdown
168
+ try {
169
+ process.kill(pid, 'SIGTERM');
170
+ logger.info('Sent SIGTERM to background process');
171
+ } catch (error) {
172
+ logger.error('Failed to send signal to background process', { error });
173
+ throw new Error('Failed to stop background recording process');
174
+ }
164
175
 
165
- logger.info('Recording stopped successfully', {
166
- outputPath: result.outputPath,
167
- duration: result.duration
168
- });
176
+ // Wait for the background process to finish and write results
177
+ logger.debug('Waiting for background process to complete...');
178
+ const maxWaitTime = 30000; // 30 seconds
179
+ const startWait = Date.now();
169
180
 
170
- // Update status to indicate recording stopped
171
- this.writeStatus({
172
- isRecording: false,
173
- completedTime: Date.now(),
174
- pid: process.pid
175
- });
181
+ while (this.isProcessRunning(pid) && (Date.now() - startWait) < maxWaitTime) {
182
+ await new Promise(resolve => setTimeout(resolve, 500));
183
+ }
176
184
 
177
- this.cleanup({ preserveResult: true });
185
+ if (this.isProcessRunning(pid)) {
186
+ logger.error('Background process did not exit within timeout, forcing kill');
187
+ try {
188
+ process.kill(pid, 'SIGKILL');
189
+ } catch (error) {
190
+ logger.error('Failed to force kill background process', { error });
191
+ }
192
+ }
193
+
194
+ logger.info('Background process stopped');
195
+
196
+ // Return a minimal result indicating success
197
+ // The upload will be handled by the stop command checking the result file
198
+ return {
199
+ outputPath: status.outputPath,
200
+ duration: Date.now() - status.startTime
201
+ };
178
202
 
179
- return result;
180
203
  } catch (error) {
181
204
  logger.error('Failed to stop recording', { error });
182
205
  throw error;
@@ -191,42 +214,90 @@ class ProcessManager {
191
214
  throw new Error('Recording already in progress');
192
215
  }
193
216
 
194
- // Import recorder module
195
- const { startRecording } = await import('./recorder.js');
217
+ // Spawn background process
218
+ const backgroundScriptPath = path.join(__dirname, '..', 'bin', 'dashcam-background.js');
196
219
 
197
- logger.info('Starting recording directly');
198
-
199
- // Start recording with the provided options
200
- const recordingOptions = {
201
- fps: parseInt(options.fps) || 10,
202
- includeAudio: options.audio || false,
203
- customOutputPath: options.output || null
204
- };
205
-
206
- logger.info('Starting recording with options', { recordingOptions });
220
+ logger.info('Starting background recording process', {
221
+ backgroundScriptPath,
222
+ options
223
+ });
207
224
 
208
- const recordingResult = await startRecording(recordingOptions);
225
+ // Serialize options to pass to background process
226
+ const optionsJson = JSON.stringify(options);
209
227
 
210
- // Write PID and status files for status tracking
211
- this.writePid(process.pid);
228
+ // Determine node executable path
229
+ const nodePath = process.execPath;
230
+
231
+ // Create log file for background process output
232
+ const logDir = path.join(PROCESS_DIR, 'logs');
233
+ if (!fs.existsSync(logDir)) {
234
+ fs.mkdirSync(logDir, { recursive: true });
235
+ }
236
+ const logFile = path.join(logDir, `recording-${Date.now()}.log`);
237
+ const logStream = fs.createWriteStream(logFile, { flags: 'a' });
212
238
 
213
- this.writeStatus({
214
- isRecording: true,
215
- startTime: recordingResult.startTime,
216
- options,
217
- pid: process.pid,
218
- outputPath: recordingResult.outputPath
239
+ logger.info('Background process log file', { logFile });
240
+
241
+ // Spawn the background process with proper detachment
242
+ const child = spawn(
243
+ nodePath,
244
+ [backgroundScriptPath, optionsJson],
245
+ {
246
+ detached: true, // Detach from parent on Unix-like systems
247
+ stdio: ['ignore', logStream, logStream], // Redirect output to log file
248
+ windowsHide: true, // Hide console window on Windows
249
+ shell: false // Don't use shell to avoid extra process wrapper
250
+ }
251
+ );
252
+
253
+ // Unref to allow parent to exit independently
254
+ child.unref();
255
+
256
+ const pid = child.pid;
257
+
258
+ logger.info('Background process spawned', {
259
+ pid,
260
+ logFile,
261
+ detached: true
219
262
  });
220
-
221
- logger.info('Recording started successfully', {
222
- outputPath: recordingResult.outputPath,
223
- startTime: recordingResult.startTime
263
+
264
+ // Wait for status file to be created by background process
265
+ logger.debug('Waiting for background process to write status file...');
266
+ const maxWaitTime = 10000; // 10 seconds
267
+ const startWait = Date.now();
268
+ let statusCreated = false;
269
+
270
+ while (!statusCreated && (Date.now() - startWait) < maxWaitTime) {
271
+ const status = this.readStatus();
272
+ if (status && status.isRecording && status.pid === pid) {
273
+ statusCreated = true;
274
+ logger.debug('Status file created by background process', { status });
275
+ break;
276
+ }
277
+ // Wait a bit before checking again
278
+ await new Promise(resolve => setTimeout(resolve, 200));
279
+ }
280
+
281
+ if (!statusCreated) {
282
+ logger.error('Background process did not create status file within timeout');
283
+ throw new Error('Failed to start background recording process - status file not created. Check log: ' + logFile);
284
+ }
285
+
286
+ // Read the status to get output path and start time
287
+ const status = this.readStatus();
288
+
289
+ logger.info('Recording started successfully in background', {
290
+ pid,
291
+ outputPath: status.outputPath,
292
+ startTime: status.startTime,
293
+ logFile
224
294
  });
225
295
 
226
296
  return {
227
- pid: process.pid,
228
- outputPath: recordingResult.outputPath,
229
- startTime: recordingResult.startTime
297
+ pid,
298
+ outputPath: status.outputPath,
299
+ startTime: status.startTime,
300
+ logFile
230
301
  };
231
302
  }
232
303
 
package/lib/recorder.js CHANGED
@@ -405,9 +405,21 @@ export async function startRecording({
405
405
  currentRecording = execa(ffmpegPath, args, {
406
406
  reject: false,
407
407
  all: true, // Capture both stdout and stderr
408
- stdin: 'pipe' // Enable stdin for sending 'q' to stop recording
408
+ stdin: 'pipe', // Enable stdin for sending 'q' to stop recording
409
+ detached: false,
410
+ windowsHide: true // Hide the console window on Windows
409
411
  });
410
412
 
413
+ // Unref the child process so it doesn't keep the parent alive
414
+ if (currentRecording.pid) {
415
+ try {
416
+ currentRecording.unref();
417
+ } catch (e) {
418
+ // Ignore errors if unref is not available
419
+ logger.debug('Could not unref ffmpeg process');
420
+ }
421
+ }
422
+
411
423
  logger.info('FFmpeg process spawned', {
412
424
  pid: currentRecording.pid,
413
425
  args: args.slice(-5), // Log last 5 args including output file
@@ -56,6 +56,13 @@ class WSServer {
56
56
  const ws = await new Promise((resolve) => {
57
57
  logger.debug('WebSocketServer: Trying port ' + port);
58
58
  const ws = new WebSocketServer({ port, host: '127.0.0.1' });
59
+
60
+ // Unref the server to prevent it from keeping the process alive
61
+ if (ws._server && ws._server.unref) {
62
+ ws._server.unref();
63
+ logger.debug('WebSocketServer: Unreffed server to allow process exit');
64
+ }
65
+
59
66
  ws.on('error', () => {
60
67
  logger.debug('WebSocketServer: Failed to listen on port ' + port);
61
68
  resolve();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dashcam",
3
- "version": "1.3.4-beta",
3
+ "version": "1.3.6-beta",
4
4
  "description": "Minimal CLI version of Dashcam desktop app",
5
5
  "main": "bin/dashcam.js",
6
6
  "bin": {