dashcam 1.0.1-beta.26 → 1.0.1-beta.28
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 +66 -32
- package/lib/processManager.js +136 -94
- package/package.json +1 -1
- package/test_workflow.sh +9 -5
package/bin/dashcam.js
CHANGED
|
@@ -136,7 +136,56 @@ async function recordingAction(options, command) {
|
|
|
136
136
|
log('Use "dashcam status" to check progress');
|
|
137
137
|
log('Use "dashcam stop" to stop recording and upload');
|
|
138
138
|
|
|
139
|
-
process
|
|
139
|
+
// Keep the process alive so recording continues
|
|
140
|
+
// Set up graceful shutdown handlers
|
|
141
|
+
const handleShutdown = async (signal) => {
|
|
142
|
+
log(`\nReceived ${signal}, stopping recording...`);
|
|
143
|
+
try {
|
|
144
|
+
const result = await processManager.stopActiveRecording();
|
|
145
|
+
|
|
146
|
+
if (result) {
|
|
147
|
+
log('Recording stopped successfully');
|
|
148
|
+
log('Uploading recording...');
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const uploadResult = await upload(result.outputPath, {
|
|
152
|
+
title: options.title || 'Dashcam Recording',
|
|
153
|
+
description: description,
|
|
154
|
+
project: options.project || options.k,
|
|
155
|
+
duration: result.duration,
|
|
156
|
+
clientStartDate: result.clientStartDate,
|
|
157
|
+
apps: result.apps,
|
|
158
|
+
icons: result.icons,
|
|
159
|
+
gifPath: result.gifPath,
|
|
160
|
+
snapshotPath: result.snapshotPath
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Write upload result for stop command to read
|
|
164
|
+
processManager.writeUploadResult({
|
|
165
|
+
shareLink: uploadResult.shareLink,
|
|
166
|
+
replayId: uploadResult.replay?.id
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
log('📹 Watch your recording:', uploadResult.shareLink);
|
|
170
|
+
} catch (uploadError) {
|
|
171
|
+
logError('Upload failed:', uploadError.message);
|
|
172
|
+
log('Recording saved locally:', result.outputPath);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
process.exit(0);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
logError('Failed to stop recording:', error.message);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
process.on('SIGINT', handleShutdown);
|
|
184
|
+
process.on('SIGTERM', handleShutdown);
|
|
185
|
+
|
|
186
|
+
// Keep process alive indefinitely until stopped
|
|
187
|
+
await new Promise(() => {}); // Wait forever
|
|
188
|
+
|
|
140
189
|
} catch (error) {
|
|
141
190
|
logError('Failed to start recording:', error.message);
|
|
142
191
|
process.exit(1);
|
|
@@ -417,58 +466,43 @@ program
|
|
|
417
466
|
|
|
418
467
|
console.log('Recording stopped successfully');
|
|
419
468
|
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
// Wait up to 2 minutes for upload result to appear
|
|
425
|
-
const maxWaitForUpload = 120000; // 2 minutes
|
|
426
|
-
const startWaitForUpload = Date.now();
|
|
427
|
-
let uploadResult = null;
|
|
428
|
-
|
|
429
|
-
while (!uploadResult && (Date.now() - startWaitForUpload) < maxWaitForUpload) {
|
|
430
|
-
uploadResult = processManager.readUploadResult();
|
|
431
|
-
if (!uploadResult) {
|
|
432
|
-
await new Promise(resolve => setTimeout(resolve, 1000)); // Check every second
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
logger.debug('Upload result read attempt', { found: !!uploadResult, shareLink: uploadResult?.shareLink });
|
|
437
|
-
|
|
438
|
-
if (uploadResult && uploadResult.shareLink) {
|
|
439
|
-
console.log('📹 Watch your recording:', uploadResult.shareLink);
|
|
440
|
-
// Clean up the result file now that we've read it
|
|
469
|
+
// Check if the result already has a shareLink (from signal handler upload)
|
|
470
|
+
if (result.shareLink) {
|
|
471
|
+
console.log('📹 Watch your recording:', result.shareLink);
|
|
441
472
|
processManager.cleanup();
|
|
442
473
|
process.exit(0);
|
|
443
474
|
}
|
|
444
475
|
|
|
445
|
-
//
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
(!result.snapshotPath || fs.existsSync(result.snapshotPath));
|
|
476
|
+
// Otherwise, check if upload result file was written
|
|
477
|
+
const uploadResultFromFile = processManager.readUploadResult();
|
|
478
|
+
logger.debug('Upload result read attempt', { found: !!uploadResultFromFile, shareLink: uploadResultFromFile?.shareLink });
|
|
449
479
|
|
|
450
|
-
if (
|
|
451
|
-
console.log('
|
|
452
|
-
|
|
480
|
+
if (uploadResultFromFile && uploadResultFromFile.shareLink) {
|
|
481
|
+
console.log('📹 Watch your recording:', uploadResultFromFile.shareLink);
|
|
482
|
+
processManager.cleanup();
|
|
453
483
|
process.exit(0);
|
|
454
484
|
}
|
|
455
485
|
|
|
456
|
-
//
|
|
486
|
+
// No upload result found - need to upload ourselves
|
|
487
|
+
// This commonly happens on Windows where signal handlers work differently
|
|
488
|
+
logger.info('No upload result found, uploading now...');
|
|
457
489
|
console.log('Uploading recording...');
|
|
490
|
+
|
|
458
491
|
try {
|
|
459
492
|
const uploadResult = await upload(result.outputPath, {
|
|
460
493
|
title: activeStatus?.options?.title,
|
|
461
494
|
description: activeStatus?.options?.description,
|
|
462
|
-
project: activeStatus?.options?.project,
|
|
495
|
+
project: activeStatus?.options?.project,
|
|
463
496
|
duration: result.duration,
|
|
464
497
|
clientStartDate: result.clientStartDate,
|
|
465
498
|
apps: result.apps,
|
|
466
|
-
|
|
499
|
+
logs: result.logs,
|
|
467
500
|
gifPath: result.gifPath,
|
|
468
501
|
snapshotPath: result.snapshotPath
|
|
469
502
|
});
|
|
470
503
|
|
|
471
504
|
console.log('📹 Watch your recording:', uploadResult.shareLink);
|
|
505
|
+
processManager.cleanup();
|
|
472
506
|
} catch (uploadError) {
|
|
473
507
|
console.error('Upload failed:', uploadError.message);
|
|
474
508
|
console.log('Recording saved locally:', result.outputPath);
|
package/lib/processManager.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
1
|
import fs from 'fs';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
import os from 'os';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
4
|
import { logger } from './logger.js';
|
|
7
5
|
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = path.dirname(__filename);
|
|
10
|
-
|
|
11
6
|
// Use a fixed directory in the user's home directory for cross-process communication
|
|
12
7
|
const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
|
|
13
8
|
const PID_FILE = path.join(PROCESS_DIR, 'recording.pid');
|
|
@@ -156,48 +151,121 @@ class ProcessManager {
|
|
|
156
151
|
return false;
|
|
157
152
|
}
|
|
158
153
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const maxWaitTime = 120000; // 2 minutes max to allow for upload
|
|
166
|
-
const startWait = Date.now();
|
|
167
|
-
|
|
168
|
-
while (this.isProcessRunning(pid) && (Date.now() - startWait) < maxWaitTime) {
|
|
169
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (this.isProcessRunning(pid)) {
|
|
173
|
-
logger.warn('Process did not stop within timeout, forcing termination');
|
|
174
|
-
process.kill(pid, 'SIGKILL');
|
|
175
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// The background process handles stopRecording() internally via SIGINT
|
|
179
|
-
// We just need to return the basic result from the status file
|
|
180
|
-
if (status) {
|
|
181
|
-
logger.info('Background recording stopped, returning status', {
|
|
182
|
-
outputPath: status.outputPath,
|
|
183
|
-
duration: Date.now() - status.startTime
|
|
184
|
-
});
|
|
154
|
+
// Check if this is the same process (direct recording)
|
|
155
|
+
if (pid === process.pid) {
|
|
156
|
+
logger.info('Stopping recording in current process');
|
|
157
|
+
|
|
158
|
+
// Import recorder module
|
|
159
|
+
const { stopRecording: stopRecorderRecording } = await import('./recorder.js');
|
|
185
160
|
|
|
186
|
-
|
|
187
|
-
const result =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
};
|
|
161
|
+
// Stop recording directly
|
|
162
|
+
const result = await stopRecorderRecording();
|
|
163
|
+
|
|
164
|
+
// Update status to indicate recording stopped
|
|
165
|
+
this.writeStatus({
|
|
166
|
+
isRecording: false,
|
|
167
|
+
completedTime: Date.now(),
|
|
168
|
+
pid: process.pid
|
|
169
|
+
});
|
|
196
170
|
|
|
197
171
|
this.cleanup({ preserveResult: true });
|
|
198
172
|
return result;
|
|
199
173
|
} else {
|
|
200
|
-
|
|
174
|
+
// Different process - send signal to stop it
|
|
175
|
+
logger.info('Stopping active recording process', { pid });
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
process.kill(pid, 'SIGINT');
|
|
179
|
+
} catch (error) {
|
|
180
|
+
logger.warn('Failed to send SIGINT, process may have already exited', { error: error.message });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Wait for the process to actually finish
|
|
184
|
+
const maxWaitTime = 120000; // 2 minutes max
|
|
185
|
+
const startWait = Date.now();
|
|
186
|
+
|
|
187
|
+
while (this.isProcessRunning(pid) && (Date.now() - startWait) < maxWaitTime) {
|
|
188
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (this.isProcessRunning(pid)) {
|
|
192
|
+
logger.warn('Process did not stop within timeout, forcing termination');
|
|
193
|
+
try {
|
|
194
|
+
process.kill(pid, 'SIGKILL');
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.warn('Failed to send SIGKILL', { error: error.message });
|
|
197
|
+
}
|
|
198
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Wait a bit for upload result file to be written by the signal handler
|
|
202
|
+
logger.debug('Waiting for upload result file...');
|
|
203
|
+
const maxWaitForUpload = 10000; // 10 seconds
|
|
204
|
+
const startWaitForUpload = Date.now();
|
|
205
|
+
let uploadResult = null;
|
|
206
|
+
|
|
207
|
+
while (!uploadResult && (Date.now() - startWaitForUpload) < maxWaitForUpload) {
|
|
208
|
+
uploadResult = this.readUploadResult();
|
|
209
|
+
if (!uploadResult) {
|
|
210
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// If upload result exists, the signal handler did the upload
|
|
215
|
+
if (uploadResult) {
|
|
216
|
+
logger.info('Upload completed by recording process');
|
|
217
|
+
if (status) {
|
|
218
|
+
const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
|
|
219
|
+
const result = {
|
|
220
|
+
outputPath: status.outputPath,
|
|
221
|
+
gifPath: `${basePath}.gif`,
|
|
222
|
+
snapshotPath: `${basePath}.png`,
|
|
223
|
+
duration: Date.now() - status.startTime,
|
|
224
|
+
clientStartDate: status.startTime,
|
|
225
|
+
apps: [],
|
|
226
|
+
logs: [],
|
|
227
|
+
shareLink: uploadResult.shareLink,
|
|
228
|
+
replayId: uploadResult.replayId
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
this.cleanup({ preserveResult: false }); // Clean up everything including result
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// If no upload result, we need to handle it ourselves
|
|
237
|
+
// This can happen on Windows where signal handlers don't work the same way
|
|
238
|
+
logger.warn('No upload result found, handling upload in stop command');
|
|
239
|
+
|
|
240
|
+
if (status) {
|
|
241
|
+
logger.info('Reconstructing result from status', {
|
|
242
|
+
outputPath: status.outputPath,
|
|
243
|
+
duration: Date.now() - status.startTime
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
|
|
247
|
+
const result = {
|
|
248
|
+
outputPath: status.outputPath,
|
|
249
|
+
gifPath: `${basePath}.gif`,
|
|
250
|
+
snapshotPath: `${basePath}.png`,
|
|
251
|
+
duration: Date.now() - status.startTime,
|
|
252
|
+
clientStartDate: status.startTime,
|
|
253
|
+
apps: [],
|
|
254
|
+
logs: []
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Don't cleanup yet - let the stop command handle upload
|
|
258
|
+
// Just update status to not recording
|
|
259
|
+
this.writeStatus({
|
|
260
|
+
isRecording: false,
|
|
261
|
+
completedTime: Date.now(),
|
|
262
|
+
pid: status.pid
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return result;
|
|
266
|
+
} else {
|
|
267
|
+
throw new Error('No status information available for active recording');
|
|
268
|
+
}
|
|
201
269
|
}
|
|
202
270
|
} catch (error) {
|
|
203
271
|
logger.error('Failed to stop recording', { error });
|
|
@@ -213,67 +281,41 @@ class ProcessManager {
|
|
|
213
281
|
throw new Error('Recording already in progress');
|
|
214
282
|
}
|
|
215
283
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// Get the path to the CLI binary
|
|
220
|
-
const binPath = path.join(__dirname, '..', 'bin', 'dashcam-background.js');
|
|
221
|
-
|
|
222
|
-
logger.debug('Background process path', { binPath, exists: fs.existsSync(binPath) });
|
|
223
|
-
|
|
224
|
-
// Create log files for background process
|
|
225
|
-
const logDir = PROCESS_DIR;
|
|
226
|
-
const stdoutLog = path.join(logDir, 'background-stdout.log');
|
|
227
|
-
const stderrLog = path.join(logDir, 'background-stderr.log');
|
|
228
|
-
|
|
229
|
-
const stdoutFd = fs.openSync(stdoutLog, 'a');
|
|
230
|
-
const stderrFd = fs.openSync(stderrLog, 'a');
|
|
231
|
-
|
|
232
|
-
// Spawn a detached process that will handle the recording
|
|
233
|
-
const backgroundProcess = spawn(process.execPath, [
|
|
234
|
-
binPath,
|
|
235
|
-
JSON.stringify(options)
|
|
236
|
-
], {
|
|
237
|
-
detached: true,
|
|
238
|
-
stdio: ['ignore', stdoutFd, stderrFd], // Log stdout and stderr
|
|
239
|
-
env: {
|
|
240
|
-
...process.env,
|
|
241
|
-
DASHCAM_BACKGROUND: 'true'
|
|
242
|
-
}
|
|
243
|
-
});
|
|
284
|
+
// Import recorder module
|
|
285
|
+
const { startRecording: startRecorderRecording } = await import('./recorder.js');
|
|
244
286
|
|
|
245
|
-
|
|
246
|
-
fs.closeSync(stdoutFd);
|
|
247
|
-
fs.closeSync(stderrFd);
|
|
248
|
-
|
|
249
|
-
// Get the background process PID before unreffing
|
|
250
|
-
const backgroundPid = backgroundProcess.pid;
|
|
287
|
+
logger.info('Starting recording directly');
|
|
251
288
|
|
|
252
|
-
//
|
|
253
|
-
|
|
289
|
+
// Start recording using the recorder module
|
|
290
|
+
const recordingOptions = {
|
|
291
|
+
fps: parseInt(options.fps) || 10,
|
|
292
|
+
includeAudio: options.audio || false,
|
|
293
|
+
customOutputPath: options.output || null
|
|
294
|
+
};
|
|
254
295
|
|
|
255
|
-
|
|
256
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
296
|
+
const result = await startRecorderRecording(recordingOptions);
|
|
257
297
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
298
|
+
// Write status to track the recording
|
|
299
|
+
this.writeStatus({
|
|
300
|
+
isRecording: true,
|
|
301
|
+
startTime: result.startTime,
|
|
302
|
+
options,
|
|
303
|
+
pid: process.pid,
|
|
304
|
+
outputPath: result.outputPath
|
|
305
|
+
});
|
|
264
306
|
|
|
265
|
-
// Write PID file so other commands can find the
|
|
266
|
-
this.writePid(
|
|
307
|
+
// Write PID file so other commands can find the recording process
|
|
308
|
+
this.writePid(process.pid);
|
|
267
309
|
|
|
268
|
-
logger.info('
|
|
269
|
-
pid:
|
|
270
|
-
outputPath:
|
|
310
|
+
logger.info('Recording started successfully', {
|
|
311
|
+
pid: process.pid,
|
|
312
|
+
outputPath: result.outputPath
|
|
271
313
|
});
|
|
272
314
|
|
|
273
315
|
return {
|
|
274
|
-
pid:
|
|
275
|
-
outputPath:
|
|
276
|
-
startTime:
|
|
316
|
+
pid: process.pid,
|
|
317
|
+
outputPath: result.outputPath,
|
|
318
|
+
startTime: result.startTime
|
|
277
319
|
};
|
|
278
320
|
}
|
|
279
321
|
|
package/package.json
CHANGED
package/test_workflow.sh
CHANGED
|
@@ -33,13 +33,16 @@ echo "✅ File tracking configured"
|
|
|
33
33
|
# 4. Start dashcam recording in background
|
|
34
34
|
echo ""
|
|
35
35
|
echo "4. Starting dashcam recording in background..."
|
|
36
|
-
|
|
36
|
+
echo "⚠️ NOTE: Recording will run in the background using nohup"
|
|
37
|
+
echo ""
|
|
38
|
+
|
|
39
|
+
# Use nohup to properly detach the process and keep it running
|
|
37
40
|
./bin/dashcam.js record --title "Sync Test Recording" --description "Testing video/log synchronization with timestamped events" > /tmp/dashcam-recording.log 2>&1 &
|
|
38
41
|
RECORD_PID=$!
|
|
39
42
|
|
|
40
43
|
# Wait for recording to initialize and log tracker to start
|
|
41
44
|
echo "Waiting for recording to initialize (PID: $RECORD_PID)..."
|
|
42
|
-
sleep
|
|
45
|
+
sleep 3
|
|
43
46
|
|
|
44
47
|
# Write first event after log tracker is fully ready
|
|
45
48
|
RECORDING_START=$(date +%s)
|
|
@@ -48,11 +51,12 @@ echo "🔴 EVENT 1: Recording START at $(date '+%H:%M:%S')"
|
|
|
48
51
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
49
52
|
echo "[EVENT 1] 🔴 Recording started with emoji at $(date '+%H:%M:%S') - TIMESTAMP: $RECORDING_START" >> "$TEMP_FILE"
|
|
50
53
|
|
|
51
|
-
# Verify recording is actually running
|
|
52
|
-
if
|
|
54
|
+
# Verify recording is actually running by checking status
|
|
55
|
+
if ./bin/dashcam.js status | grep -q "Recording in progress"; then
|
|
53
56
|
echo "✅ Recording started successfully"
|
|
54
57
|
else
|
|
55
|
-
echo "❌ Recording
|
|
58
|
+
echo "❌ Recording failed to start, check /tmp/dashcam-recording.log"
|
|
59
|
+
cat /tmp/dashcam-recording.log
|
|
56
60
|
exit 1
|
|
57
61
|
fi
|
|
58
62
|
|