dashcam 1.0.1-beta.34 → 1.0.1-beta.36

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.
@@ -15,6 +15,7 @@ import os from 'os';
15
15
  const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
16
16
  const STATUS_FILE = path.join(PROCESS_DIR, 'status.json');
17
17
  const RESULT_FILE = path.join(PROCESS_DIR, 'upload-result.json');
18
+ const STOP_SIGNAL_FILE = path.join(PROCESS_DIR, 'stop-signal.json');
18
19
 
19
20
  // Parse options from command line argument
20
21
  const optionsJson = process.argv[2];
@@ -96,79 +97,74 @@ async function runBackgroundRecording() {
96
97
  startTime: recordingResult.startTime
97
98
  });
98
99
 
99
- // Set up signal handlers for graceful shutdown
100
- const handleShutdown = async (signal) => {
101
- if (isShuttingDown) {
102
- logger.info('Shutdown already in progress...');
103
- return;
104
- }
105
- isShuttingDown = true;
106
-
107
- logger.info(`Received ${signal} signal, stopping background recording...`, { pid: process.pid });
108
-
100
+ // Poll for stop signal file instead of relying on system signals
101
+ // This works reliably across all platforms including Windows
102
+ logger.info('Background recording is now running. Polling for stop signal...');
103
+
104
+ const pollInterval = 1000; // Check every second
105
+
106
+ while (true) {
109
107
  try {
110
- // Stop the recording
111
- logger.info('Calling stopRecording...');
112
- const stopResult = await stopRecording();
113
-
114
- if (stopResult) {
115
- logger.info('Recording stopped successfully', {
116
- outputPath: stopResult.outputPath,
117
- duration: stopResult.duration
118
- });
108
+ if (fs.existsSync(STOP_SIGNAL_FILE)) {
109
+ if (isShuttingDown) {
110
+ logger.info('Shutdown already in progress...');
111
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
112
+ continue;
113
+ }
114
+ isShuttingDown = true;
119
115
 
120
- // Upload the recording
121
- logger.info('Starting upload...');
122
- const uploadResult = await upload(stopResult.outputPath, {
123
- title: options.title || 'Dashcam Recording',
124
- description: options.description || 'Recorded with Dashcam CLI',
125
- project: options.project || options.k,
126
- duration: stopResult.duration,
127
- clientStartDate: stopResult.clientStartDate,
128
- apps: stopResult.apps,
129
- logs: stopResult.logs,
130
- gifPath: stopResult.gifPath,
131
- snapshotPath: stopResult.snapshotPath
132
- });
116
+ logger.info('Stop signal file detected, initiating graceful shutdown');
117
+
118
+ // Read and log the signal
119
+ try {
120
+ const signalData = JSON.parse(fs.readFileSync(STOP_SIGNAL_FILE, 'utf8'));
121
+ logger.info('Stop signal received', signalData);
122
+ } catch (e) {
123
+ logger.warn('Could not parse stop signal file');
124
+ }
125
+
126
+ // Stop the recording
127
+ logger.info('Calling stopRecording...');
128
+ const stopResult = await stopRecording();
133
129
 
134
- logger.info('Upload complete', { shareLink: uploadResult.shareLink });
130
+ if (stopResult) {
131
+ logger.info('Recording stopped successfully', {
132
+ outputPath: stopResult.outputPath,
133
+ duration: stopResult.duration
134
+ });
135
+
136
+ // Write the recording result so stop command can upload it
137
+ logger.info('Writing recording result for stop command to upload');
138
+ writeUploadResult({
139
+ outputPath: stopResult.outputPath,
140
+ duration: stopResult.duration,
141
+ clientStartDate: stopResult.clientStartDate,
142
+ apps: stopResult.apps,
143
+ logs: stopResult.logs,
144
+ gifPath: stopResult.gifPath,
145
+ snapshotPath: stopResult.snapshotPath,
146
+ recordingStopped: true
147
+ });
148
+ logger.info('Recording result written successfully');
149
+ }
135
150
 
136
- // Write upload result for stop command to read
137
- logger.info('About to write upload result...');
138
- writeUploadResult({
139
- shareLink: uploadResult.shareLink,
140
- replayId: uploadResult.replay?.id
151
+ // Update status to indicate recording stopped
152
+ writeStatus({
153
+ isRecording: false,
154
+ completedTime: Date.now(),
155
+ pid: process.pid
141
156
  });
142
- logger.info('Upload result written successfully');
157
+
158
+ logger.info('Background process exiting successfully');
159
+ process.exit(0);
143
160
  }
144
-
145
- // Update status to indicate recording stopped
146
- writeStatus({
147
- isRecording: false,
148
- completedTime: Date.now(),
149
- pid: process.pid
150
- });
151
-
152
- logger.info('Background process exiting successfully');
153
- process.exit(0);
154
161
  } catch (error) {
155
- logger.error('Error during shutdown:', { error: error.message, stack: error.stack });
156
- process.exit(1);
162
+ logger.error('Error during stop signal handling', { error: error.message, stack: error.stack });
157
163
  }
158
- };
159
-
160
- process.on('SIGINT', () => {
161
- logger.info('SIGINT handler triggered');
162
- handleShutdown('SIGINT');
163
- });
164
- process.on('SIGTERM', () => {
165
- logger.info('SIGTERM handler triggered');
166
- handleShutdown('SIGTERM');
167
- });
168
-
169
- // Keep the process alive
170
- logger.info('Background recording is now running. Waiting for stop signal...');
171
- await new Promise(() => {}); // Wait indefinitely for signals
164
+
165
+ // Wait before next poll
166
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
167
+ }
172
168
 
173
169
  } catch (error) {
174
170
  logger.error('Background recording failed:', error);
package/bin/dashcam.js CHANGED
@@ -419,61 +419,56 @@ program
419
419
 
420
420
  console.log('Recording stopped successfully');
421
421
 
422
- // Wait for upload to complete (background process handles this)
423
- logger.debug('Waiting for background upload to complete...');
424
- console.log('⏳ Uploading recording...');
422
+ // Wait for background process to finish recording and write result
423
+ logger.debug('Waiting for background process to finish recording...');
425
424
 
426
- // Wait up to 2 minutes for upload result to appear
427
- const maxWaitForUpload = 120000; // 2 minutes
428
- const startWaitForUpload = Date.now();
429
- let uploadResult = null;
425
+ // Wait up to 30 seconds for recording result to appear
426
+ const maxWaitForRecording = 30000;
427
+ const startWaitForRecording = Date.now();
428
+ let recordingResult = null;
430
429
 
431
- while (!uploadResult && (Date.now() - startWaitForUpload) < maxWaitForUpload) {
432
- uploadResult = processManager.readUploadResult();
433
- if (!uploadResult) {
434
- await new Promise(resolve => setTimeout(resolve, 1000)); // Check every second
430
+ while (!recordingResult && (Date.now() - startWaitForRecording) < maxWaitForRecording) {
431
+ const resultData = processManager.readUploadResult();
432
+ if (resultData && resultData.recordingStopped) {
433
+ recordingResult = resultData;
434
+ logger.debug('Recording result received from background process');
435
+ break;
435
436
  }
437
+ await new Promise(resolve => setTimeout(resolve, 500)); // Check every 500ms
436
438
  }
437
439
 
438
- logger.debug('Upload result read attempt', { found: !!uploadResult, shareLink: uploadResult?.shareLink });
439
-
440
- if (uploadResult && uploadResult.shareLink) {
441
- console.log('📹 Watch your recording:', uploadResult.shareLink);
442
- // Clean up the result file now that we've read it
443
- processManager.cleanup();
444
- process.exit(0);
445
- }
446
-
447
- // Check if files still exist - if not, background process already uploaded
448
- const filesExist = fs.existsSync(result.outputPath) &&
449
- (!result.gifPath || fs.existsSync(result.gifPath)) &&
450
- (!result.snapshotPath || fs.existsSync(result.snapshotPath));
451
-
452
- if (!filesExist) {
453
- console.log('✅ Recording uploaded by background process');
454
- logger.info('Files were cleaned up by background process');
455
- process.exit(0);
440
+ if (!recordingResult || !recordingResult.outputPath) {
441
+ logger.error('Failed to get recording result from background process');
442
+ console.log('⚠️ Failed to get recording from background process');
443
+ process.exit(1);
456
444
  }
457
445
 
458
- // Always attempt to upload - let upload function find project if needed
446
+ // Now upload the recording ourselves
447
+ logger.info('Uploading recording from stop command', { outputPath: recordingResult.outputPath });
459
448
  console.log('Uploading recording...');
449
+
460
450
  try {
461
- const uploadResult = await upload(result.outputPath, {
462
- title: activeStatus?.options?.title,
451
+ const uploadResult = await upload(recordingResult.outputPath, {
452
+ title: activeStatus?.options?.title || 'Dashcam Recording',
463
453
  description: activeStatus?.options?.description,
464
- project: activeStatus?.options?.project, // May be undefined, that's ok
465
- duration: result.duration,
466
- clientStartDate: result.clientStartDate,
467
- apps: result.apps,
468
- icons: result.icons,
469
- gifPath: result.gifPath,
470
- snapshotPath: result.snapshotPath
454
+ project: activeStatus?.options?.project,
455
+ duration: recordingResult.duration,
456
+ clientStartDate: recordingResult.clientStartDate,
457
+ apps: recordingResult.apps,
458
+ logs: recordingResult.logs,
459
+ gifPath: recordingResult.gifPath,
460
+ snapshotPath: recordingResult.snapshotPath
471
461
  });
472
462
 
473
463
  console.log('📹 Watch your recording:', uploadResult.shareLink);
464
+
465
+ // Clean up result file
466
+ processManager.cleanup();
467
+ process.exit(0);
474
468
  } catch (uploadError) {
475
469
  console.error('Upload failed:', uploadError.message);
476
- console.log('Recording saved locally:', result.outputPath);
470
+ console.log('Recording saved locally:', recordingResult.outputPath);
471
+ process.exit(1);
477
472
  }
478
473
  } catch (error) {
479
474
  console.error('Failed to stop recording:', error.message);
@@ -7,9 +7,9 @@ import { logger } from './logger.js';
7
7
 
8
8
  // Use a fixed directory in the user's home directory for cross-process communication
9
9
  const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
10
- const PID_FILE = path.join(PROCESS_DIR, 'recording.pid');
11
10
  const STATUS_FILE = path.join(PROCESS_DIR, 'status.json');
12
11
  const RESULT_FILE = path.join(PROCESS_DIR, 'upload-result.json');
12
+ const STOP_SIGNAL_FILE = path.join(PROCESS_DIR, 'stop-signal.json');
13
13
 
14
14
  // Ensure process directory exists
15
15
  if (!fs.existsSync(PROCESS_DIR)) {
@@ -42,7 +42,15 @@ class ProcessManager {
42
42
  try {
43
43
  if (!fs.existsSync(STATUS_FILE)) return null;
44
44
  const data = fs.readFileSync(STATUS_FILE, 'utf8');
45
- return JSON.parse(data);
45
+ const status = JSON.parse(data);
46
+
47
+ // Check if status is stale (older than 24 hours)
48
+ if (status.timestamp && (Date.now() - status.timestamp) > 24 * 60 * 60 * 1000) {
49
+ logger.warn('Status file is stale, ignoring');
50
+ return null;
51
+ }
52
+
53
+ return status;
46
54
  } catch (error) {
47
55
  logger.error('Failed to read status file', { error });
48
56
  return null;
@@ -83,45 +91,22 @@ class ProcessManager {
83
91
  }
84
92
  }
85
93
 
86
- writePid(pid = process.pid) {
87
- try {
88
- fs.writeFileSync(PID_FILE, pid.toString());
89
- } catch (error) {
90
- logger.error('Failed to write PID file', { error });
91
- }
92
- }
93
-
94
- readPid() {
95
- try {
96
- if (!fs.existsSync(PID_FILE)) return null;
97
- const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
98
- return isNaN(pid) ? null : pid;
99
- } catch (error) {
100
- return null;
101
- }
102
- }
103
-
104
- isProcessRunning(pid) {
105
- if (!pid) return false;
106
- try {
107
- process.kill(pid, 0); // Signal 0 just checks if process exists
108
- return true;
109
- } catch (error) {
110
- return false;
111
- }
112
- }
113
-
114
94
  isRecordingActive() {
115
- const pid = this.readPid();
116
95
  const status = this.readStatus();
117
96
 
118
- if (!pid || !this.isProcessRunning(pid)) {
119
- // Clean up but preserve upload result in case the background process just finished uploading
97
+ // Recording is active if we have a status file with isRecording=true
98
+ // and it's not stale
99
+ if (status && status.isRecording) {
100
+ return true;
101
+ }
102
+
103
+ // Clean up stale files if recording is not active
104
+ if (!status || !status.isRecording) {
120
105
  this.cleanup({ preserveResult: true });
121
106
  return false;
122
107
  }
123
108
 
124
- return status && status.isRecording;
109
+ return false;
125
110
  }
126
111
 
127
112
  getActiveStatus() {
@@ -133,14 +118,14 @@ class ProcessManager {
133
118
  const { preserveResult = false } = options;
134
119
  try {
135
120
  logger.debug('Cleanup called', { preserveResult, resultFileExists: fs.existsSync(RESULT_FILE) });
136
- if (fs.existsSync(PID_FILE)) {
137
- fs.unlinkSync(PID_FILE);
138
- logger.debug('Deleted PID file');
139
- }
140
121
  if (fs.existsSync(STATUS_FILE)) {
141
122
  fs.unlinkSync(STATUS_FILE);
142
123
  logger.debug('Deleted STATUS file');
143
124
  }
125
+ if (fs.existsSync(STOP_SIGNAL_FILE)) {
126
+ fs.unlinkSync(STOP_SIGNAL_FILE);
127
+ logger.debug('Deleted STOP_SIGNAL file');
128
+ }
144
129
  if (!preserveResult && fs.existsSync(RESULT_FILE)) {
145
130
  fs.unlinkSync(RESULT_FILE);
146
131
  logger.debug('Deleted RESULT file');
@@ -161,63 +146,35 @@ class ProcessManager {
161
146
  this.isStopping = true;
162
147
 
163
148
  try {
164
- const pid = this.readPid();
165
149
  const status = this.readStatus();
166
150
 
167
- if (!pid || !this.isProcessRunning(pid)) {
168
- logger.warn('No active recording process found');
151
+ if (!status || !status.isRecording) {
152
+ logger.warn('No active recording found in status file');
169
153
  return false;
170
154
  }
171
155
 
172
- logger.info('Stopping detached background process', { pid });
173
-
174
- // Send signal to background process
175
- const isWindows = process.platform === 'win32';
176
-
177
- if (isWindows) {
178
- logger.info('Windows detected, attempting graceful shutdown first');
179
- try {
180
- // First try graceful shutdown without /F (force) flag
181
- // This sends CTRL+C which Node.js can handle
182
- const { execSync } = await import('child_process');
183
- try {
184
- execSync(`taskkill /PID ${pid} /T`, { stdio: 'ignore', timeout: 5000 });
185
- logger.info('Sent graceful shutdown signal to process');
186
- } catch (error) {
187
- // If graceful shutdown fails or times out, force kill
188
- logger.warn('Graceful shutdown failed, forcing termination');
189
- execSync(`taskkill /PID ${pid} /F /T`, { stdio: 'ignore' });
190
- }
191
- } catch (error) {
192
- logger.warn('Failed to kill process with taskkill', { error: error.message });
193
- }
194
- } else {
195
- try {
196
- process.kill(pid, 'SIGINT');
197
- } catch (error) {
198
- logger.warn('Failed to send SIGINT', { error: error.message });
199
- }
200
- }
201
-
202
- // Wait for the process to actually finish
203
- const maxWaitTime = 30000; // 30 seconds
204
- const startWait = Date.now();
156
+ logger.info('Signaling background process to stop recording');
205
157
 
206
- while (this.isProcessRunning(pid) && (Date.now() - startWait) < maxWaitTime) {
207
- await new Promise(resolve => setTimeout(resolve, 500));
158
+ // Write stop signal file instead of killing the process
159
+ // The background process polls for this file
160
+ try {
161
+ fs.writeFileSync(STOP_SIGNAL_FILE, JSON.stringify({
162
+ timestamp: Date.now(),
163
+ requestedBy: 'stop-command'
164
+ }));
165
+ logger.info('Stop signal file written successfully');
166
+ } catch (error) {
167
+ logger.error('Failed to write stop signal file', { error: error.message });
168
+ throw new Error('Failed to signal background process to stop');
208
169
  }
209
170
 
210
- if (this.isProcessRunning(pid)) {
211
- logger.warn('Process did not stop within timeout');
212
- if (!isWindows) {
213
- try {
214
- process.kill(pid, 'SIGKILL');
215
- } catch (error) {
216
- logger.warn('Failed to send SIGKILL', { error: error.message });
217
- }
218
- await new Promise(resolve => setTimeout(resolve, 1000));
219
- }
220
- }
171
+ // Mark recording as stopping in status file
172
+ this.writeStatus({
173
+ isRecording: false,
174
+ stopping: true,
175
+ stoppedAt: Date.now(),
176
+ pid: status.pid
177
+ });
221
178
 
222
179
  if (status) {
223
180
  logger.info('Recording stopped, returning status', {
@@ -236,7 +193,8 @@ class ProcessManager {
236
193
  logs: []
237
194
  };
238
195
 
239
- this.cleanup({ preserveResult: true });
196
+ // Don't cleanup here - the background process needs to read the stop signal file
197
+ // Cleanup will happen after we get the recording result
240
198
  return result;
241
199
  } else {
242
200
  throw new Error('No status information available for active recording');
@@ -272,9 +230,6 @@ class ProcessManager {
272
230
  child.unref();
273
231
 
274
232
  const pid = child.pid;
275
-
276
- // Write PID file immediately so other commands can find the recording process
277
- this.writePid(pid);
278
233
 
279
234
  logger.info('Background process spawned successfully', {
280
235
  pid,
@@ -282,17 +237,24 @@ class ProcessManager {
282
237
  });
283
238
 
284
239
  // Wait a moment for the background process to write its status
285
- await new Promise(resolve => setTimeout(resolve, 1000));
240
+ await new Promise(resolve => setTimeout(resolve, 1500));
286
241
 
287
242
  // Read the status to get output path and start time
288
243
  const status = this.readStatus();
289
244
 
290
245
  if (!status) {
246
+ logger.error('Background process failed to write status');
291
247
  throw new Error('Background process failed to write status');
292
248
  }
293
249
 
250
+ logger.info('Recording started successfully', {
251
+ pid: status.pid,
252
+ outputPath: status.outputPath,
253
+ startTime: status.startTime
254
+ });
255
+
294
256
  return {
295
- pid,
257
+ pid: status.pid,
296
258
  outputPath: status.outputPath,
297
259
  startTime: status.startTime
298
260
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dashcam",
3
- "version": "1.0.1-beta.34",
3
+ "version": "1.0.1-beta.36",
4
4
  "description": "Minimal CLI version of Dashcam desktop app",
5
5
  "main": "bin/index.js",
6
6
  "bin": {