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.
- package/bin/dashcam-background.js +61 -65
- package/bin/dashcam.js +35 -40
- package/lib/processManager.js +55 -93
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
// Update status to indicate recording stopped
|
|
152
|
+
writeStatus({
|
|
153
|
+
isRecording: false,
|
|
154
|
+
completedTime: Date.now(),
|
|
155
|
+
pid: process.pid
|
|
141
156
|
});
|
|
142
|
-
|
|
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
|
|
156
|
-
process.exit(1);
|
|
162
|
+
logger.error('Error during stop signal handling', { error: error.message, stack: error.stack });
|
|
157
163
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
423
|
-
logger.debug('Waiting for background
|
|
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
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
let
|
|
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 (!
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
//
|
|
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(
|
|
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,
|
|
465
|
-
duration:
|
|
466
|
-
clientStartDate:
|
|
467
|
-
apps:
|
|
468
|
-
|
|
469
|
-
gifPath:
|
|
470
|
-
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:',
|
|
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);
|
package/lib/processManager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
119
|
-
|
|
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
|
|
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 (!
|
|
168
|
-
logger.warn('No active recording
|
|
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('
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
};
|