dashcam 1.1.0-beta.5 → 1.3.0-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/.github/workflows/publish.yml +1 -15
- package/bin/dashcam-background.js +177 -0
- package/bin/dashcam.js +424 -69
- package/lib/config.js +7 -1
- package/lib/processManager.js +306 -0
- package/package.json +2 -2
- package/test_workflow.sh +15 -24
|
@@ -8,7 +8,7 @@ on:
|
|
|
8
8
|
jobs:
|
|
9
9
|
publish-beta:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
steps:
|
|
13
13
|
- uses: actions/checkout@v4
|
|
14
14
|
with:
|
|
@@ -25,39 +25,25 @@ jobs:
|
|
|
25
25
|
git config --global user.name "github-actions[bot]"
|
|
26
26
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
27
27
|
|
|
28
|
-
- name: Check if this is a version bump commit
|
|
29
|
-
id: check_commit
|
|
30
|
-
run: |
|
|
31
|
-
if echo "${{ github.event.head_commit.message }}" | grep -q "chore: bump beta version"; then
|
|
32
|
-
echo "skip=true" >> $GITHUB_OUTPUT
|
|
33
|
-
else
|
|
34
|
-
echo "skip=false" >> $GITHUB_OUTPUT
|
|
35
|
-
fi
|
|
36
|
-
|
|
37
28
|
- name: Install dependencies
|
|
38
|
-
if: steps.check_commit.outputs.skip != 'true'
|
|
39
29
|
run: npm install
|
|
40
30
|
|
|
41
31
|
- name: Commit dependency changes
|
|
42
|
-
if: steps.check_commit.outputs.skip != 'true'
|
|
43
32
|
run: |
|
|
44
33
|
git add package-lock.json
|
|
45
34
|
git diff --staged --quiet || git commit -m "chore: update package-lock.json"
|
|
46
35
|
|
|
47
36
|
- name: Bump version (prerelease beta)
|
|
48
|
-
if: steps.check_commit.outputs.skip != 'true'
|
|
49
37
|
run: |
|
|
50
38
|
npm version prerelease --preid=beta -m "chore: bump beta version to %s"
|
|
51
39
|
env:
|
|
52
40
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
53
41
|
|
|
54
42
|
- name: Publish to npm under beta tag
|
|
55
|
-
if: steps.check_commit.outputs.skip != 'true'
|
|
56
43
|
run: npm publish --tag beta
|
|
57
44
|
env:
|
|
58
45
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
59
46
|
|
|
60
47
|
- name: Push version bump commit + tag
|
|
61
|
-
if: steps.check_commit.outputs.skip != 'true'
|
|
62
48
|
run: |
|
|
63
49
|
git push --follow-tags
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Background recording process for dashcam CLI
|
|
4
|
+
* This script runs detached from the parent process to handle long-running recordings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { startRecording, stopRecording } from '../lib/recorder.js';
|
|
8
|
+
import { upload } from '../lib/uploader.js';
|
|
9
|
+
import { logger, setVerbose } from '../lib/logger.js';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
|
|
14
|
+
// Get process directory for status files
|
|
15
|
+
const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
|
|
16
|
+
const STATUS_FILE = path.join(PROCESS_DIR, 'status.json');
|
|
17
|
+
const RESULT_FILE = path.join(PROCESS_DIR, 'upload-result.json');
|
|
18
|
+
|
|
19
|
+
// Parse options from command line argument
|
|
20
|
+
const optionsJson = process.argv[2];
|
|
21
|
+
if (!optionsJson) {
|
|
22
|
+
console.error('No options provided to background process');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const options = JSON.parse(optionsJson);
|
|
27
|
+
|
|
28
|
+
// Enable verbose logging in background
|
|
29
|
+
setVerbose(true);
|
|
30
|
+
|
|
31
|
+
logger.info('Background recording process started', {
|
|
32
|
+
pid: process.pid,
|
|
33
|
+
options
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Write status file
|
|
37
|
+
function writeStatus(status) {
|
|
38
|
+
try {
|
|
39
|
+
fs.writeFileSync(STATUS_FILE, JSON.stringify({
|
|
40
|
+
...status,
|
|
41
|
+
timestamp: Date.now(),
|
|
42
|
+
pid: process.pid
|
|
43
|
+
}, null, 2));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.error('Failed to write status file', { error });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Write upload result file
|
|
50
|
+
function writeUploadResult(result) {
|
|
51
|
+
try {
|
|
52
|
+
logger.info('Writing upload result to file', { path: RESULT_FILE, shareLink: result.shareLink });
|
|
53
|
+
fs.writeFileSync(RESULT_FILE, JSON.stringify({
|
|
54
|
+
...result,
|
|
55
|
+
timestamp: Date.now()
|
|
56
|
+
}, null, 2));
|
|
57
|
+
logger.info('Successfully wrote upload result to file');
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.error('Failed to write upload result file', { error });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Main recording function
|
|
64
|
+
async function runBackgroundRecording() {
|
|
65
|
+
let recordingResult = null;
|
|
66
|
+
let isShuttingDown = false;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Start the recording
|
|
70
|
+
const recordingOptions = {
|
|
71
|
+
fps: parseInt(options.fps) || 10,
|
|
72
|
+
includeAudio: options.audio || false,
|
|
73
|
+
customOutputPath: options.output || null
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
logger.info('Starting recording with options', { recordingOptions });
|
|
77
|
+
|
|
78
|
+
recordingResult = await startRecording(recordingOptions);
|
|
79
|
+
|
|
80
|
+
// Write status to track the recording
|
|
81
|
+
writeStatus({
|
|
82
|
+
isRecording: true,
|
|
83
|
+
startTime: recordingResult.startTime,
|
|
84
|
+
options,
|
|
85
|
+
pid: process.pid,
|
|
86
|
+
outputPath: recordingResult.outputPath
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
logger.info('Recording started successfully', {
|
|
90
|
+
outputPath: recordingResult.outputPath,
|
|
91
|
+
startTime: recordingResult.startTime
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Set up signal handlers for graceful shutdown
|
|
95
|
+
const handleShutdown = async (signal) => {
|
|
96
|
+
if (isShuttingDown) {
|
|
97
|
+
logger.info('Shutdown already in progress...');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
isShuttingDown = true;
|
|
101
|
+
|
|
102
|
+
logger.info(`Received ${signal}, stopping background recording...`);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Stop the recording
|
|
106
|
+
const stopResult = await stopRecording();
|
|
107
|
+
|
|
108
|
+
if (stopResult) {
|
|
109
|
+
logger.info('Recording stopped successfully', {
|
|
110
|
+
outputPath: stopResult.outputPath,
|
|
111
|
+
duration: stopResult.duration
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Upload the recording
|
|
115
|
+
logger.info('Starting upload...');
|
|
116
|
+
const uploadResult = await upload(stopResult.outputPath, {
|
|
117
|
+
title: options.title || 'Dashcam Recording',
|
|
118
|
+
description: options.description || 'Recorded with Dashcam CLI',
|
|
119
|
+
project: options.project || options.k,
|
|
120
|
+
duration: stopResult.duration,
|
|
121
|
+
clientStartDate: stopResult.clientStartDate,
|
|
122
|
+
apps: stopResult.apps,
|
|
123
|
+
logs: stopResult.logs,
|
|
124
|
+
gifPath: stopResult.gifPath,
|
|
125
|
+
snapshotPath: stopResult.snapshotPath
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
logger.info('Upload complete', { shareLink: uploadResult.shareLink });
|
|
129
|
+
|
|
130
|
+
// Write upload result for stop command to read
|
|
131
|
+
writeUploadResult({
|
|
132
|
+
shareLink: uploadResult.shareLink,
|
|
133
|
+
replayId: uploadResult.replay?.id
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Update status to indicate recording stopped
|
|
138
|
+
writeStatus({
|
|
139
|
+
isRecording: false,
|
|
140
|
+
completedTime: Date.now(),
|
|
141
|
+
pid: process.pid
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
logger.info('Background process exiting successfully');
|
|
145
|
+
process.exit(0);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.error('Error during shutdown:', error);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
153
|
+
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
154
|
+
|
|
155
|
+
// Keep the process alive
|
|
156
|
+
logger.info('Background recording is now running. Waiting for stop signal...');
|
|
157
|
+
await new Promise(() => {}); // Wait indefinitely for signals
|
|
158
|
+
|
|
159
|
+
} catch (error) {
|
|
160
|
+
logger.error('Background recording failed:', error);
|
|
161
|
+
|
|
162
|
+
// Update status to indicate failure
|
|
163
|
+
writeStatus({
|
|
164
|
+
isRecording: false,
|
|
165
|
+
error: error.message,
|
|
166
|
+
pid: process.pid
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Run the background recording
|
|
174
|
+
runBackgroundRecording().catch(error => {
|
|
175
|
+
logger.error('Fatal error in background process:', error);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
package/bin/dashcam.js
CHANGED
|
@@ -5,7 +5,7 @@ import { upload } from '../lib/uploader.js';
|
|
|
5
5
|
import { logger, setVerbose } from '../lib/logger.js';
|
|
6
6
|
import { APP } from '../lib/config.js';
|
|
7
7
|
import { createPattern } from '../lib/tracking.js';
|
|
8
|
-
import {
|
|
8
|
+
import { processManager } from '../lib/processManager.js';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import { dirname } from 'path';
|
|
11
11
|
import path from 'path';
|
|
@@ -83,13 +83,14 @@ async function recordingAction(options, command) {
|
|
|
83
83
|
const logError = (...args) => { if (!silent) console.error(...args); };
|
|
84
84
|
|
|
85
85
|
// Check if recording is already active
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const duration = (status.
|
|
86
|
+
if (processManager.isRecordingActive()) {
|
|
87
|
+
const status = processManager.getActiveStatus();
|
|
88
|
+
const duration = ((Date.now() - status.startTime) / 1000).toFixed(1);
|
|
89
89
|
log('Recording already in progress');
|
|
90
90
|
log(`Duration: ${duration} seconds`);
|
|
91
|
-
log(
|
|
92
|
-
|
|
91
|
+
log(`PID: ${status.pid}`);
|
|
92
|
+
log('Use "dashcam stop" to stop the recording');
|
|
93
|
+
process.exit(0);
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
// Check authentication
|
|
@@ -116,77 +117,26 @@ async function recordingAction(options, command) {
|
|
|
116
117
|
process.exit(1);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
// Start recording
|
|
120
|
-
log('Starting recording...');
|
|
120
|
+
// Start recording in background mode
|
|
121
|
+
log('Starting recording in background...');
|
|
121
122
|
|
|
122
123
|
try {
|
|
123
|
-
const
|
|
124
|
+
const result = await processManager.startRecording({
|
|
124
125
|
fps: parseInt(options.fps) || 30,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const recordingMetadata = {
|
|
126
|
+
audio: options.audio,
|
|
127
|
+
output: options.output,
|
|
130
128
|
title: options.title,
|
|
131
129
|
description: description,
|
|
132
|
-
project: options.project || options.k
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
await startRecording(recordingOptions);
|
|
130
|
+
project: options.project || options.k // Support both -p and -k for project
|
|
131
|
+
});
|
|
136
132
|
|
|
137
|
-
log(`✅ Recording started successfully`);
|
|
133
|
+
log(`✅ Recording started successfully (PID: ${result.pid})`);
|
|
134
|
+
log(`Output: ${result.outputPath}`);
|
|
138
135
|
log('');
|
|
139
|
-
log('
|
|
140
|
-
|
|
141
|
-
// Set up graceful shutdown handlers
|
|
142
|
-
const handleShutdown = async (signal) => {
|
|
143
|
-
log('\nStopping recording...');
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
const result = await stopRecording();
|
|
147
|
-
|
|
148
|
-
if (!result) {
|
|
149
|
-
log('Failed to stop recording');
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
log('Recording stopped successfully');
|
|
154
|
-
|
|
155
|
-
// Upload the recording
|
|
156
|
-
log('Uploading recording...');
|
|
157
|
-
try {
|
|
158
|
-
const uploadResult = await upload(result.outputPath, {
|
|
159
|
-
title: recordingMetadata.title || 'Dashcam Recording',
|
|
160
|
-
description: recordingMetadata.description,
|
|
161
|
-
project: recordingMetadata.project,
|
|
162
|
-
duration: result.duration,
|
|
163
|
-
clientStartDate: result.clientStartDate,
|
|
164
|
-
apps: result.apps,
|
|
165
|
-
icons: result.icons,
|
|
166
|
-
logs: result.logs,
|
|
167
|
-
gifPath: result.gifPath,
|
|
168
|
-
snapshotPath: result.snapshotPath
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
log('📹 Watch your recording:', uploadResult.shareLink);
|
|
172
|
-
process.exit(0);
|
|
173
|
-
} catch (uploadError) {
|
|
174
|
-
logError('Upload failed:', uploadError.message);
|
|
175
|
-
log('Recording saved locally:', result.outputPath);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
}
|
|
178
|
-
} catch (error) {
|
|
179
|
-
logError('Failed to stop recording:', error.message);
|
|
180
|
-
process.exit(1);
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
185
|
-
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
186
|
-
|
|
187
|
-
// Keep process alive
|
|
188
|
-
await new Promise(() => {});
|
|
136
|
+
log('Use "dashcam status" to check progress');
|
|
137
|
+
log('Use "dashcam stop" to stop recording and upload');
|
|
189
138
|
|
|
139
|
+
process.exit(0);
|
|
190
140
|
} catch (error) {
|
|
191
141
|
logError('Failed to start recording:', error.message);
|
|
192
142
|
process.exit(1);
|
|
@@ -198,6 +148,83 @@ async function recordingAction(options, command) {
|
|
|
198
148
|
}
|
|
199
149
|
}
|
|
200
150
|
|
|
151
|
+
// 'create' command - creates a clip from current recording (like stop but with more options)
|
|
152
|
+
program
|
|
153
|
+
.command('create')
|
|
154
|
+
.description('Create a clip and output the resulting url or markdown. Will launch desktop app for local editing before publishing.')
|
|
155
|
+
.option('-t, --title <string>', 'Title of the replay. Automatically generated if not supplied.')
|
|
156
|
+
.option('-d, --description [text]', 'Replay markdown body. This may also be piped in: `cat README.md | dashcam create`')
|
|
157
|
+
.option('--md', 'Returns code for a rich markdown image link.')
|
|
158
|
+
.option('-k, --project <project>', 'Project ID to publish to')
|
|
159
|
+
.action(async (options) => {
|
|
160
|
+
try {
|
|
161
|
+
// Check for piped input (description from stdin)
|
|
162
|
+
let description = options.description;
|
|
163
|
+
if (!description && !process.stdin.isTTY) {
|
|
164
|
+
const chunks = [];
|
|
165
|
+
for await (const chunk of process.stdin) {
|
|
166
|
+
chunks.push(chunk);
|
|
167
|
+
}
|
|
168
|
+
description = Buffer.concat(chunks).toString('utf-8');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!processManager.isRecordingActive()) {
|
|
172
|
+
console.log('No active recording to create clip from');
|
|
173
|
+
console.log('Start a recording first with "dashcam record" or "dashcam start"');
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const activeStatus = processManager.getActiveStatus();
|
|
178
|
+
|
|
179
|
+
console.log('Creating clip from recording...');
|
|
180
|
+
|
|
181
|
+
const result = await processManager.stopActiveRecording();
|
|
182
|
+
|
|
183
|
+
if (!result) {
|
|
184
|
+
console.log('Failed to stop recording');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log('Recording stopped successfully');
|
|
189
|
+
|
|
190
|
+
// Upload the recording
|
|
191
|
+
console.log('Uploading clip...');
|
|
192
|
+
try {
|
|
193
|
+
const uploadResult = await upload(result.outputPath, {
|
|
194
|
+
title: options.title || activeStatus?.options?.title || 'Dashcam Recording',
|
|
195
|
+
description: description || activeStatus?.options?.description,
|
|
196
|
+
project: options.project || options.k || activeStatus?.options?.project,
|
|
197
|
+
duration: result.duration,
|
|
198
|
+
clientStartDate: result.clientStartDate,
|
|
199
|
+
apps: result.apps,
|
|
200
|
+
icons: result.icons,
|
|
201
|
+
gifPath: result.gifPath,
|
|
202
|
+
snapshotPath: result.snapshotPath
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Output based on format option
|
|
206
|
+
if (options.md) {
|
|
207
|
+
const replayId = uploadResult.replay?.id;
|
|
208
|
+
const shareKey = uploadResult.shareLink.split('share=')[1];
|
|
209
|
+
console.log(`[](${uploadResult.shareLink})`);
|
|
210
|
+
console.log('');
|
|
211
|
+
console.log(`Watch [Dashcam - ${options.title || 'New Replay'}](${uploadResult.shareLink}) on Dashcam`);
|
|
212
|
+
} else {
|
|
213
|
+
console.log(uploadResult.shareLink);
|
|
214
|
+
}
|
|
215
|
+
} catch (uploadError) {
|
|
216
|
+
console.error('Upload failed:', uploadError.message);
|
|
217
|
+
console.log('Recording saved locally:', result.outputPath);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
process.exit(0);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
logger.error('Error creating clip:', error);
|
|
223
|
+
console.error('Failed to create clip:', error.message);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
201
228
|
// 'record' command - the main recording command with all options
|
|
202
229
|
program
|
|
203
230
|
.command('record')
|
|
@@ -211,6 +238,254 @@ program
|
|
|
211
238
|
.option('-s, --silent', 'Silent mode - suppress all output')
|
|
212
239
|
.action(recordingAction);
|
|
213
240
|
|
|
241
|
+
program
|
|
242
|
+
.command('pipe')
|
|
243
|
+
.description('Pipe command output to dashcam to be included in recorded video')
|
|
244
|
+
.action(async () => {
|
|
245
|
+
try {
|
|
246
|
+
// Check if recording is active
|
|
247
|
+
if (!processManager.isRecordingActive()) {
|
|
248
|
+
console.error('No active recording. Start a recording first with "dashcam record" or "dashcam start"');
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Read from stdin
|
|
253
|
+
const chunks = [];
|
|
254
|
+
for await (const chunk of process.stdin) {
|
|
255
|
+
chunks.push(chunk);
|
|
256
|
+
// Also output to stdout so pipe continues to work
|
|
257
|
+
process.stdout.write(chunk);
|
|
258
|
+
}
|
|
259
|
+
const content = Buffer.concat(chunks).toString('utf-8');
|
|
260
|
+
|
|
261
|
+
// Import the log tracker to add the piped content
|
|
262
|
+
const { logsTrackerManager } = await import('../lib/logs/index.js');
|
|
263
|
+
|
|
264
|
+
// Add piped content as a log entry
|
|
265
|
+
logsTrackerManager.addPipedLog(content);
|
|
266
|
+
|
|
267
|
+
process.exit(0);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
logger.error('Failed to pipe content:', error);
|
|
270
|
+
console.error('Failed to pipe content:', error.message);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
program
|
|
276
|
+
.command('status')
|
|
277
|
+
.description('Show current recording status')
|
|
278
|
+
.action(() => {
|
|
279
|
+
const activeStatus = processManager.getActiveStatus();
|
|
280
|
+
if (activeStatus) {
|
|
281
|
+
const duration = ((Date.now() - activeStatus.startTime) / 1000).toFixed(1);
|
|
282
|
+
console.log('Recording in progress');
|
|
283
|
+
console.log(`Duration: ${duration} seconds`);
|
|
284
|
+
console.log(`PID: ${activeStatus.pid}`);
|
|
285
|
+
console.log(`Started: ${new Date(activeStatus.startTime).toLocaleString()}`);
|
|
286
|
+
if (activeStatus.options.title) {
|
|
287
|
+
console.log(`Title: ${activeStatus.options.title}`);
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
console.log('No active recording');
|
|
291
|
+
}
|
|
292
|
+
process.exit(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
// 'start' command - alias for record with simple instant replay mode
|
|
298
|
+
program
|
|
299
|
+
.command('start')
|
|
300
|
+
.description('Start instant replay recording on dashcam')
|
|
301
|
+
.action(async () => {
|
|
302
|
+
// Call recordingAction with minimal options for instant replay
|
|
303
|
+
await recordingAction({
|
|
304
|
+
fps: '30',
|
|
305
|
+
audio: false,
|
|
306
|
+
silent: false
|
|
307
|
+
}, null);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
program
|
|
311
|
+
.command('track')
|
|
312
|
+
.description('Add a logs config to Dashcam')
|
|
313
|
+
.option('--name <name>', 'Name for the tracking configuration (required)')
|
|
314
|
+
.option('--type <type>', 'Type of tracker: "application" or "web" (required)')
|
|
315
|
+
.option('--pattern <pattern>', 'Pattern to track (can be used multiple times)', (value, previous) => {
|
|
316
|
+
return previous ? previous.concat([value]) : [value];
|
|
317
|
+
})
|
|
318
|
+
.option('--web <pattern>', 'Web URL pattern to track (can use wildcards like *) - deprecated, use --type=web --pattern instead')
|
|
319
|
+
.option('--app <pattern>', 'Application file pattern to track (can use wildcards like *) - deprecated, use --type=application --pattern instead')
|
|
320
|
+
.action(async (options) => {
|
|
321
|
+
try {
|
|
322
|
+
// Support both old and new syntax
|
|
323
|
+
// New syntax: --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"
|
|
324
|
+
// Old syntax: --web <pattern> --app <pattern>
|
|
325
|
+
|
|
326
|
+
if (options.type && options.pattern) {
|
|
327
|
+
// New syntax validation
|
|
328
|
+
if (!options.name) {
|
|
329
|
+
console.error('Error: --name is required when using --type and --pattern');
|
|
330
|
+
console.log('Example: dashcam track --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (options.type !== 'web' && options.type !== 'application') {
|
|
335
|
+
console.error('Error: --type must be either "web" or "application"');
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const config = {
|
|
340
|
+
name: options.name,
|
|
341
|
+
type: options.type,
|
|
342
|
+
patterns: options.pattern,
|
|
343
|
+
enabled: true
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
await createPattern(config);
|
|
347
|
+
console.log(`${options.type === 'web' ? 'Web' : 'Application'} tracking pattern added successfully:`, options.name);
|
|
348
|
+
console.log('Patterns:', options.pattern.join(', '));
|
|
349
|
+
|
|
350
|
+
} else if (options.web || options.app) {
|
|
351
|
+
// Old syntax for backward compatibility
|
|
352
|
+
if (options.web) {
|
|
353
|
+
const config = {
|
|
354
|
+
name: options.name || 'Web Pattern',
|
|
355
|
+
type: 'web',
|
|
356
|
+
patterns: [options.web],
|
|
357
|
+
enabled: true
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
await createPattern(config);
|
|
361
|
+
console.log('Web tracking pattern added successfully:', options.web);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (options.app) {
|
|
365
|
+
const config = {
|
|
366
|
+
name: options.name || 'App Pattern',
|
|
367
|
+
type: 'application',
|
|
368
|
+
patterns: [options.app],
|
|
369
|
+
enabled: true
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
await createPattern(config);
|
|
373
|
+
console.log('Application tracking pattern added successfully:', options.app);
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
console.error('Error: Must provide either:');
|
|
377
|
+
console.log(' --name --type --pattern (new syntax)');
|
|
378
|
+
console.log(' --web or --app (old syntax)');
|
|
379
|
+
console.log('\nExamples:');
|
|
380
|
+
console.log(' dashcam track --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
|
|
381
|
+
console.log(' dashcam track --web "*facebook.com*"');
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
process.exit(0);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.error('Failed to add tracking pattern:', error.message);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
program
|
|
393
|
+
.command('stop')
|
|
394
|
+
.description('Stop the current recording and wait for upload completion')
|
|
395
|
+
.action(async () => {
|
|
396
|
+
try {
|
|
397
|
+
// Enable verbose logging for stop command
|
|
398
|
+
setVerbose(true);
|
|
399
|
+
|
|
400
|
+
if (!processManager.isRecordingActive()) {
|
|
401
|
+
console.log('No active recording to stop');
|
|
402
|
+
process.exit(0);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const activeStatus = processManager.getActiveStatus();
|
|
406
|
+
const logFile = path.join(process.cwd(), '.dashcam', 'recording.log');
|
|
407
|
+
|
|
408
|
+
console.log('Stopping recording...');
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const result = await processManager.stopActiveRecording();
|
|
412
|
+
|
|
413
|
+
if (!result) {
|
|
414
|
+
console.log('Failed to stop recording');
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
console.log('Recording stopped successfully');
|
|
419
|
+
|
|
420
|
+
// Wait for upload to complete (background process handles this)
|
|
421
|
+
logger.debug('Waiting for background upload to complete...');
|
|
422
|
+
console.log('⏳ Uploading recording...');
|
|
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
|
|
441
|
+
processManager.cleanup();
|
|
442
|
+
process.exit(0);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Check if files still exist - if not, background process already uploaded
|
|
446
|
+
const filesExist = fs.existsSync(result.outputPath) &&
|
|
447
|
+
(!result.gifPath || fs.existsSync(result.gifPath)) &&
|
|
448
|
+
(!result.snapshotPath || fs.existsSync(result.snapshotPath));
|
|
449
|
+
|
|
450
|
+
if (!filesExist) {
|
|
451
|
+
console.log('✅ Recording uploaded by background process');
|
|
452
|
+
logger.info('Files were cleaned up by background process');
|
|
453
|
+
process.exit(0);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Always attempt to upload - let upload function find project if needed
|
|
457
|
+
console.log('Uploading recording...');
|
|
458
|
+
try {
|
|
459
|
+
const uploadResult = await upload(result.outputPath, {
|
|
460
|
+
title: activeStatus?.options?.title,
|
|
461
|
+
description: activeStatus?.options?.description,
|
|
462
|
+
project: activeStatus?.options?.project, // May be undefined, that's ok
|
|
463
|
+
duration: result.duration,
|
|
464
|
+
clientStartDate: result.clientStartDate,
|
|
465
|
+
apps: result.apps,
|
|
466
|
+
icons: result.icons,
|
|
467
|
+
gifPath: result.gifPath,
|
|
468
|
+
snapshotPath: result.snapshotPath
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
console.log('📹 Watch your recording:', uploadResult.shareLink);
|
|
472
|
+
} catch (uploadError) {
|
|
473
|
+
console.error('Upload failed:', uploadError.message);
|
|
474
|
+
console.log('Recording saved locally:', result.outputPath);
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
console.error('Failed to stop recording:', error.message);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
process.exit(0);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
logger.error('Error stopping recording:', error);
|
|
484
|
+
console.error('Failed to stop recording:', error.message);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
214
489
|
program
|
|
215
490
|
.command('logs')
|
|
216
491
|
.description('Manage log tracking for recordings')
|
|
@@ -337,4 +612,84 @@ program
|
|
|
337
612
|
}
|
|
338
613
|
});
|
|
339
614
|
|
|
615
|
+
program
|
|
616
|
+
.command('upload')
|
|
617
|
+
.description('Upload a completed recording file or recover from interrupted recording')
|
|
618
|
+
.argument('[filePath]', 'Path to the recording file to upload (optional)')
|
|
619
|
+
.option('-t, --title <title>', 'Title for the recording')
|
|
620
|
+
.option('-d, --description <description>', 'Description for the recording')
|
|
621
|
+
.option('-p, --project <project>', 'Project ID to upload to')
|
|
622
|
+
.option('--recover', 'Attempt to recover and upload from interrupted recording')
|
|
623
|
+
.action(async (filePath, options) => {
|
|
624
|
+
try {
|
|
625
|
+
let targetFile = filePath;
|
|
626
|
+
|
|
627
|
+
if (options.recover) {
|
|
628
|
+
// Try to recover from interrupted recording
|
|
629
|
+
const tempFileInfoPath = path.join(process.cwd(), '.dashcam', 'temp-file.json');
|
|
630
|
+
|
|
631
|
+
if (fs.existsSync(tempFileInfoPath)) {
|
|
632
|
+
console.log('Found interrupted recording, attempting recovery...');
|
|
633
|
+
|
|
634
|
+
const tempFileInfo = JSON.parse(fs.readFileSync(tempFileInfoPath, 'utf8'));
|
|
635
|
+
const tempFile = tempFileInfo.tempFile;
|
|
636
|
+
|
|
637
|
+
if (fs.existsSync(tempFile) && fs.statSync(tempFile).size > 0) {
|
|
638
|
+
console.log('Recovering recording from temp file...');
|
|
639
|
+
|
|
640
|
+
// Import recorder to finalize the interrupted recording
|
|
641
|
+
const { stopRecording } = await import('../lib/recorder.js');
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
// This will attempt to finalize the temp file
|
|
645
|
+
const result = await stopRecording();
|
|
646
|
+
targetFile = result.outputPath;
|
|
647
|
+
console.log('Recovery successful:', result.outputPath);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
console.error('Recovery failed:', error.message);
|
|
650
|
+
console.log('You can try uploading the temp file directly:', tempFile);
|
|
651
|
+
targetFile = tempFile;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Clean up temp file info after recovery attempt
|
|
655
|
+
fs.unlinkSync(tempFileInfoPath);
|
|
656
|
+
} else {
|
|
657
|
+
console.log('No valid temp file found for recovery');
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
} else {
|
|
661
|
+
console.log('No interrupted recording found');
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!targetFile) {
|
|
667
|
+
console.error('Please provide a file path or use --recover option');
|
|
668
|
+
console.log('Examples:');
|
|
669
|
+
console.log(' dashcam upload /path/to/recording.webm');
|
|
670
|
+
console.log(' dashcam upload --recover');
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (!fs.existsSync(targetFile)) {
|
|
675
|
+
console.error('File not found:', targetFile);
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
console.log('Uploading recording...');
|
|
680
|
+
const uploadResult = await upload(targetFile, {
|
|
681
|
+
title: options.title,
|
|
682
|
+
description: options.description,
|
|
683
|
+
project: options.project
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
console.log('✅ Upload complete! Share link:', uploadResult.shareLink);
|
|
687
|
+
process.exit(0);
|
|
688
|
+
|
|
689
|
+
} catch (error) {
|
|
690
|
+
console.error('Upload failed:', error.message);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
340
695
|
program.parse();
|
package/lib/config.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { fileURLToPath } from 'url';
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
4
5
|
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
7
|
const __dirname = dirname(__filename);
|
|
7
8
|
|
|
9
|
+
// Read version from package.json
|
|
10
|
+
const packageJson = JSON.parse(
|
|
11
|
+
readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')
|
|
12
|
+
);
|
|
13
|
+
|
|
8
14
|
export const ENV = process.env.NODE_ENV || 'production';
|
|
9
15
|
|
|
10
16
|
export const auth0Config = {
|
|
@@ -26,7 +32,7 @@ export const API_ENDPOINT = apiEndpoints[ENV];
|
|
|
26
32
|
export const APP = {
|
|
27
33
|
id: 'dashcam-cli',
|
|
28
34
|
name: ENV === 'production' ? 'Dashcam CLI' : `Dashcam CLI - ${ENV}`,
|
|
29
|
-
version:
|
|
35
|
+
version: packageJson.version,
|
|
30
36
|
configDir: join(homedir(), '.dashcam'),
|
|
31
37
|
logsDir: join(homedir(), '.dashcam', 'logs'),
|
|
32
38
|
recordingsDir: join(homedir(), '.dashcam', 'recordings'),
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
// Use a fixed directory in the user's home directory for cross-process communication
|
|
12
|
+
const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
|
|
13
|
+
const PID_FILE = path.join(PROCESS_DIR, 'recording.pid');
|
|
14
|
+
const STATUS_FILE = path.join(PROCESS_DIR, 'status.json');
|
|
15
|
+
const RESULT_FILE = path.join(PROCESS_DIR, 'upload-result.json');
|
|
16
|
+
|
|
17
|
+
// Ensure process directory exists
|
|
18
|
+
if (!fs.existsSync(PROCESS_DIR)) {
|
|
19
|
+
fs.mkdirSync(PROCESS_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class ProcessManager {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.isBackgroundMode = false;
|
|
25
|
+
this.isStopping = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setBackgroundMode(enabled = true) {
|
|
29
|
+
this.isBackgroundMode = enabled;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
writeStatus(status) {
|
|
33
|
+
try {
|
|
34
|
+
fs.writeFileSync(STATUS_FILE, JSON.stringify({
|
|
35
|
+
...status,
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
pid: process.pid
|
|
38
|
+
}, null, 2));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
logger.error('Failed to write status file', { error });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
readStatus() {
|
|
45
|
+
try {
|
|
46
|
+
if (!fs.existsSync(STATUS_FILE)) return null;
|
|
47
|
+
const data = fs.readFileSync(STATUS_FILE, 'utf8');
|
|
48
|
+
return JSON.parse(data);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.error('Failed to read status file', { error });
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
writeUploadResult(result) {
|
|
56
|
+
try {
|
|
57
|
+
logger.info('Writing upload result to file', { path: RESULT_FILE, shareLink: result.shareLink });
|
|
58
|
+
fs.writeFileSync(RESULT_FILE, JSON.stringify({
|
|
59
|
+
...result,
|
|
60
|
+
timestamp: Date.now()
|
|
61
|
+
}, null, 2));
|
|
62
|
+
logger.info('Successfully wrote upload result to file');
|
|
63
|
+
// Verify it was written
|
|
64
|
+
if (fs.existsSync(RESULT_FILE)) {
|
|
65
|
+
logger.info('Verified upload result file exists');
|
|
66
|
+
} else {
|
|
67
|
+
logger.error('Upload result file does not exist after write!');
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error('Failed to write upload result file', { error });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
readUploadResult() {
|
|
75
|
+
try {
|
|
76
|
+
if (!fs.existsSync(RESULT_FILE)) return null;
|
|
77
|
+
const data = fs.readFileSync(RESULT_FILE, 'utf8');
|
|
78
|
+
return JSON.parse(data);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger.error('Failed to read upload result file', { error });
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
writePid(pid = process.pid) {
|
|
86
|
+
try {
|
|
87
|
+
fs.writeFileSync(PID_FILE, pid.toString());
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.error('Failed to write PID file', { error });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
readPid() {
|
|
94
|
+
try {
|
|
95
|
+
if (!fs.existsSync(PID_FILE)) return null;
|
|
96
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
|
|
97
|
+
return isNaN(pid) ? null : pid;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
isProcessRunning(pid) {
|
|
104
|
+
if (!pid) return false;
|
|
105
|
+
try {
|
|
106
|
+
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
107
|
+
return true;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
isRecordingActive() {
|
|
114
|
+
const pid = this.readPid();
|
|
115
|
+
const status = this.readStatus();
|
|
116
|
+
|
|
117
|
+
if (!pid || !this.isProcessRunning(pid)) {
|
|
118
|
+
// Clean up but preserve upload result in case the background process just finished uploading
|
|
119
|
+
this.cleanup({ preserveResult: true });
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return status && status.isRecording;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getActiveStatus() {
|
|
127
|
+
if (!this.isRecordingActive()) return null;
|
|
128
|
+
return this.readStatus();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
cleanup(options = {}) {
|
|
132
|
+
const { preserveResult = false } = options;
|
|
133
|
+
try {
|
|
134
|
+
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
|
|
135
|
+
if (fs.existsSync(STATUS_FILE)) fs.unlinkSync(STATUS_FILE);
|
|
136
|
+
if (!preserveResult && fs.existsSync(RESULT_FILE)) fs.unlinkSync(RESULT_FILE);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger.error('Failed to cleanup process files', { error });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async stopActiveRecording() {
|
|
143
|
+
if (this.isStopping) {
|
|
144
|
+
logger.info('Stop already in progress, ignoring additional stop request');
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.isStopping = true;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const pid = this.readPid();
|
|
152
|
+
const status = this.readStatus();
|
|
153
|
+
|
|
154
|
+
if (!pid || !this.isProcessRunning(pid)) {
|
|
155
|
+
logger.warn('No active recording process found');
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Recording is active, send SIGINT to trigger graceful shutdown
|
|
160
|
+
logger.info('Stopping active recording process', { pid });
|
|
161
|
+
process.kill(pid, 'SIGINT');
|
|
162
|
+
|
|
163
|
+
// Wait for the process to actually finish and upload
|
|
164
|
+
// Increase timeout to allow for upload to complete
|
|
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
|
+
});
|
|
185
|
+
|
|
186
|
+
const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
|
|
187
|
+
const result = {
|
|
188
|
+
outputPath: status.outputPath,
|
|
189
|
+
gifPath: `${basePath}.gif`,
|
|
190
|
+
snapshotPath: `${basePath}.png`,
|
|
191
|
+
duration: Date.now() - status.startTime,
|
|
192
|
+
clientStartDate: status.startTime,
|
|
193
|
+
apps: [],
|
|
194
|
+
logs: []
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
this.cleanup({ preserveResult: true });
|
|
198
|
+
return result;
|
|
199
|
+
} else {
|
|
200
|
+
throw new Error('No status information available for active recording');
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
logger.error('Failed to stop recording', { error });
|
|
204
|
+
throw error;
|
|
205
|
+
} finally {
|
|
206
|
+
this.isStopping = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async startRecording(options) {
|
|
211
|
+
// Check if recording is already active
|
|
212
|
+
if (this.isRecordingActive()) {
|
|
213
|
+
throw new Error('Recording already in progress');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Always run in background mode by spawning a detached process
|
|
217
|
+
logger.info('Starting recording in background mode');
|
|
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
|
+
// Always use 'ignore' for stdio to ensure complete detachment
|
|
230
|
+
// This is critical when parent process is invoked via exec() or other methods
|
|
231
|
+
// that might have open stdio pipes
|
|
232
|
+
const isWindows = process.platform === 'win32';
|
|
233
|
+
|
|
234
|
+
// Spawn a detached process that will handle the recording
|
|
235
|
+
const backgroundProcess = spawn(process.execPath, [
|
|
236
|
+
binPath,
|
|
237
|
+
JSON.stringify(options)
|
|
238
|
+
], {
|
|
239
|
+
detached: true,
|
|
240
|
+
stdio: 'ignore', // Always ignore to prevent hanging when parent has open pipes
|
|
241
|
+
windowsHide: true, // Hide the console window on Windows
|
|
242
|
+
env: {
|
|
243
|
+
...process.env,
|
|
244
|
+
DASHCAM_BACKGROUND: 'true'
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Get the background process PID before unreffing
|
|
249
|
+
const backgroundPid = backgroundProcess.pid;
|
|
250
|
+
|
|
251
|
+
// Write PID file immediately so other commands can find the background process
|
|
252
|
+
// Use the spawned process PID rather than waiting for status file
|
|
253
|
+
this.writePid(backgroundPid);
|
|
254
|
+
|
|
255
|
+
// Allow the parent process to exit independently
|
|
256
|
+
backgroundProcess.unref();
|
|
257
|
+
|
|
258
|
+
// Wait a moment for the background process to initialize
|
|
259
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
260
|
+
|
|
261
|
+
// Read the status file to get recording details
|
|
262
|
+
const status = this.readStatus();
|
|
263
|
+
|
|
264
|
+
if (!status || !status.isRecording) {
|
|
265
|
+
// Clean up PID file if recording failed to start
|
|
266
|
+
this.cleanup();
|
|
267
|
+
throw new Error('Background process failed to start recording');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
logger.info('Background recording process started', {
|
|
271
|
+
pid: status.pid,
|
|
272
|
+
outputPath: status.outputPath
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
pid: status.pid,
|
|
277
|
+
outputPath: status.outputPath,
|
|
278
|
+
startTime: status.startTime
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async gracefulExit() {
|
|
283
|
+
logger.info('Graceful exit requested');
|
|
284
|
+
|
|
285
|
+
// If we're currently recording, stop it properly
|
|
286
|
+
if (this.isRecordingActive()) {
|
|
287
|
+
try {
|
|
288
|
+
logger.info('Stopping active recording before exit');
|
|
289
|
+
await this.stopActiveRecording();
|
|
290
|
+
logger.info('Recording stopped successfully during graceful exit');
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.error('Failed to stop recording during graceful exit', { error });
|
|
293
|
+
this.cleanup(); // Fallback cleanup
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// Just cleanup if no recording is active
|
|
297
|
+
this.cleanup();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const processManager = new ProcessManager();
|
|
305
|
+
|
|
306
|
+
export { processManager };
|
package/package.json
CHANGED
package/test_workflow.sh
CHANGED
|
@@ -30,16 +30,16 @@ if [ ! -f "$TEMP_FILE" ]; then
|
|
|
30
30
|
fi
|
|
31
31
|
echo "✅ File tracking configured"
|
|
32
32
|
|
|
33
|
-
# 4. Start dashcam recording in background
|
|
33
|
+
# 4. Start dashcam recording in background
|
|
34
34
|
echo ""
|
|
35
|
-
echo "4. Starting dashcam recording..."
|
|
36
|
-
# Start recording
|
|
35
|
+
echo "4. Starting dashcam recording in background..."
|
|
36
|
+
# Start recording and redirect output to a log file so we can still monitor it
|
|
37
37
|
./bin/dashcam.js record --title "Sync Test Recording" --description "Testing video/log synchronization with timestamped events" > /tmp/dashcam-recording.log 2>&1 &
|
|
38
38
|
RECORD_PID=$!
|
|
39
39
|
|
|
40
40
|
# Wait for recording to initialize and log tracker to start
|
|
41
41
|
echo "Waiting for recording to initialize (PID: $RECORD_PID)..."
|
|
42
|
-
sleep
|
|
42
|
+
sleep 1
|
|
43
43
|
|
|
44
44
|
# Write first event after log tracker is fully ready
|
|
45
45
|
RECORDING_START=$(date +%s)
|
|
@@ -53,7 +53,6 @@ if ps -p $RECORD_PID > /dev/null; then
|
|
|
53
53
|
echo "✅ Recording started successfully"
|
|
54
54
|
else
|
|
55
55
|
echo "❌ Recording process died, check /tmp/dashcam-recording.log"
|
|
56
|
-
cat /tmp/dashcam-recording.log
|
|
57
56
|
exit 1
|
|
58
57
|
fi
|
|
59
58
|
|
|
@@ -110,28 +109,15 @@ echo ""
|
|
|
110
109
|
echo "Waiting 2 seconds to ensure all events are captured..."
|
|
111
110
|
sleep 2
|
|
112
111
|
|
|
113
|
-
# 6. Stop recording
|
|
112
|
+
# 6. Stop recording and upload (this will kill the background recording process)
|
|
114
113
|
echo ""
|
|
115
|
-
echo "6. Stopping recording
|
|
114
|
+
echo "6. Stopping recording and uploading..."
|
|
116
115
|
# Check if recording is still active
|
|
117
|
-
if
|
|
118
|
-
|
|
119
|
-
echo "
|
|
120
|
-
|
|
121
|
-
# Wait for the process to finish (it will stop recording and upload)
|
|
122
|
-
wait $RECORD_PID 2>/dev/null || true
|
|
123
|
-
|
|
124
|
-
echo "✅ Recording stopped and upload completed"
|
|
125
|
-
|
|
126
|
-
# Show the output from the recording process
|
|
127
|
-
echo ""
|
|
128
|
-
echo "Recording output:"
|
|
129
|
-
cat /tmp/dashcam-recording.log
|
|
116
|
+
if ./bin/dashcam.js status | grep -q "Recording in progress"; then
|
|
117
|
+
./bin/dashcam.js stop
|
|
118
|
+
echo "✅ Recording stopped and uploaded"
|
|
130
119
|
else
|
|
131
|
-
echo "⚠️ Recording
|
|
132
|
-
echo ""
|
|
133
|
-
echo "Recording output:"
|
|
134
|
-
cat /tmp/dashcam-recording.log
|
|
120
|
+
echo "⚠️ Recording already completed (this is expected with background mode)"
|
|
135
121
|
fi
|
|
136
122
|
|
|
137
123
|
echo ""
|
|
@@ -141,6 +127,11 @@ echo ""
|
|
|
141
127
|
echo "🎉 Test workflow completed successfully!"
|
|
142
128
|
echo "======================================"
|
|
143
129
|
|
|
130
|
+
# Show final status
|
|
131
|
+
echo ""
|
|
132
|
+
echo "📊 Final Status:"
|
|
133
|
+
./bin/dashcam.js status
|
|
134
|
+
|
|
144
135
|
echo ""
|
|
145
136
|
echo "╔════════════════════════════════════════════════════════════════╗"
|
|
146
137
|
echo "║ SYNC VERIFICATION GUIDE ║"
|