dashcam 0.8.4 → 1.0.1-beta.2
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/.dashcam/cli-config.json +3 -0
- package/.dashcam/recording.log +135 -0
- package/.dashcam/web-config.json +11 -0
- package/.github/RELEASE.md +59 -0
- package/.github/workflows/build.yml +103 -0
- package/.github/workflows/publish.yml +43 -0
- package/.github/workflows/release.yml +107 -0
- package/LOG_TRACKING_GUIDE.md +225 -0
- package/README.md +709 -155
- package/bin/dashcam.cjs +8 -0
- package/bin/dashcam.js +542 -0
- package/bin/index.js +63 -0
- package/examples/execute-script.js +152 -0
- package/examples/simple-test.js +37 -0
- package/lib/applicationTracker.js +311 -0
- package/lib/auth.js +222 -0
- package/lib/binaries.js +21 -0
- package/lib/config.js +34 -0
- package/lib/extension-logs/helpers.js +182 -0
- package/lib/extension-logs/index.js +347 -0
- package/lib/extension-logs/manager.js +344 -0
- package/lib/ffmpeg.js +156 -0
- package/lib/logTracker.js +23 -0
- package/lib/logger.js +118 -0
- package/lib/logs/index.js +432 -0
- package/lib/permissions.js +85 -0
- package/lib/processManager.js +255 -0
- package/lib/recorder.js +675 -0
- package/lib/store.js +58 -0
- package/lib/tracking/FileTracker.js +98 -0
- package/lib/tracking/FileTrackerManager.js +62 -0
- package/lib/tracking/LogsTracker.js +147 -0
- package/lib/tracking/active-win.js +212 -0
- package/lib/tracking/icons/darwin.js +39 -0
- package/lib/tracking/icons/index.js +167 -0
- package/lib/tracking/icons/windows.js +27 -0
- package/lib/tracking/idle.js +82 -0
- package/lib/tracking.js +23 -0
- package/lib/uploader.js +449 -0
- package/lib/utilities/jsonl.js +77 -0
- package/lib/webLogsDaemon.js +234 -0
- package/lib/websocket/server.js +223 -0
- package/package.json +50 -21
- package/recording.log +814 -0
- package/sea-bundle.mjs +34595 -0
- package/test-page.html +15 -0
- package/test.log +1 -0
- package/test_run.log +48 -0
- package/test_workflow.sh +80 -0
- package/examples/crash-test.js +0 -11
- package/examples/github-issue.sh +0 -1
- package/examples/protocol.html +0 -22
- package/index.js +0 -158
- package/lib.js +0 -199
- package/recorder.js +0 -85
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
|
|
7
|
+
// Use a fixed directory in the user's home directory for cross-process communication
|
|
8
|
+
const PROCESS_DIR = path.join(os.homedir(), '.dashcam-cli');
|
|
9
|
+
const PID_FILE = path.join(PROCESS_DIR, 'recording.pid');
|
|
10
|
+
const STATUS_FILE = path.join(PROCESS_DIR, 'status.json');
|
|
11
|
+
|
|
12
|
+
// Ensure process directory exists
|
|
13
|
+
if (!fs.existsSync(PROCESS_DIR)) {
|
|
14
|
+
fs.mkdirSync(PROCESS_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class ProcessManager {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.isBackgroundMode = false;
|
|
20
|
+
this.isStopping = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setBackgroundMode(enabled = true) {
|
|
24
|
+
this.isBackgroundMode = enabled;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
writeStatus(status) {
|
|
28
|
+
try {
|
|
29
|
+
fs.writeFileSync(STATUS_FILE, JSON.stringify({
|
|
30
|
+
...status,
|
|
31
|
+
timestamp: Date.now(),
|
|
32
|
+
pid: process.pid
|
|
33
|
+
}, null, 2));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
logger.error('Failed to write status file', { error });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
readStatus() {
|
|
40
|
+
try {
|
|
41
|
+
if (!fs.existsSync(STATUS_FILE)) return null;
|
|
42
|
+
const data = fs.readFileSync(STATUS_FILE, 'utf8');
|
|
43
|
+
return JSON.parse(data);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.error('Failed to read status file', { error });
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
writePid(pid = process.pid) {
|
|
51
|
+
try {
|
|
52
|
+
fs.writeFileSync(PID_FILE, pid.toString());
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.error('Failed to write PID file', { error });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
readPid() {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(PID_FILE)) return null;
|
|
61
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
|
|
62
|
+
return isNaN(pid) ? null : pid;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isProcessRunning(pid) {
|
|
69
|
+
if (!pid) return false;
|
|
70
|
+
try {
|
|
71
|
+
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
72
|
+
return true;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isRecordingActive() {
|
|
79
|
+
const pid = this.readPid();
|
|
80
|
+
const status = this.readStatus();
|
|
81
|
+
|
|
82
|
+
if (!pid || !this.isProcessRunning(pid)) {
|
|
83
|
+
this.cleanup();
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return status && status.isRecording;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getActiveStatus() {
|
|
91
|
+
if (!this.isRecordingActive()) return null;
|
|
92
|
+
return this.readStatus();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
cleanup() {
|
|
96
|
+
try {
|
|
97
|
+
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
|
|
98
|
+
if (fs.existsSync(STATUS_FILE)) fs.unlinkSync(STATUS_FILE);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logger.error('Failed to cleanup process files', { error });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async stopActiveRecording() {
|
|
105
|
+
if (this.isStopping) {
|
|
106
|
+
logger.info('Stop already in progress, ignoring additional stop request');
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.isStopping = true;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const pid = this.readPid();
|
|
114
|
+
const status = this.readStatus();
|
|
115
|
+
|
|
116
|
+
if (!pid || !this.isProcessRunning(pid)) {
|
|
117
|
+
logger.warn('No active recording process found');
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Recording is active, send SIGINT to trigger graceful shutdown
|
|
122
|
+
logger.info('Stopping active recording process', { pid });
|
|
123
|
+
process.kill(pid, 'SIGINT');
|
|
124
|
+
|
|
125
|
+
// Wait for the process to actually finish
|
|
126
|
+
const maxWaitTime = 30000; // 30 seconds max
|
|
127
|
+
const startWait = Date.now();
|
|
128
|
+
|
|
129
|
+
while (this.isProcessRunning(pid) && (Date.now() - startWait) < maxWaitTime) {
|
|
130
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (this.isProcessRunning(pid)) {
|
|
134
|
+
logger.warn('Process did not stop within timeout, forcing termination');
|
|
135
|
+
process.kill(pid, 'SIGKILL');
|
|
136
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Call the actual recorder's stopRecording function to get complete results
|
|
140
|
+
if (status) {
|
|
141
|
+
try {
|
|
142
|
+
const { stopRecording } = await import('./recorder.js');
|
|
143
|
+
const result = await stopRecording();
|
|
144
|
+
|
|
145
|
+
logger.info('Recording stopped successfully via recorder', {
|
|
146
|
+
outputPath: result.outputPath,
|
|
147
|
+
duration: result.duration,
|
|
148
|
+
hasLogs: result.logs?.length > 0,
|
|
149
|
+
hasApps: result.apps?.length > 0
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Cleanup process files
|
|
153
|
+
this.cleanup();
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
} catch (recorderError) {
|
|
157
|
+
logger.warn('Failed to stop via recorder, falling back to basic result', { error: recorderError.message });
|
|
158
|
+
|
|
159
|
+
// Fallback to basic result if recorder fails
|
|
160
|
+
const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
|
|
161
|
+
const result = {
|
|
162
|
+
outputPath: status.outputPath,
|
|
163
|
+
gifPath: `${basePath}.gif`,
|
|
164
|
+
snapshotPath: `${basePath}.png`,
|
|
165
|
+
duration: Date.now() - status.startTime,
|
|
166
|
+
clientStartDate: status.startTime,
|
|
167
|
+
apps: [],
|
|
168
|
+
logs: []
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
this.cleanup();
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
throw new Error('No status information available for active recording');
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
logger.error('Failed to stop recording', { error });
|
|
179
|
+
throw error;
|
|
180
|
+
} finally {
|
|
181
|
+
this.isStopping = false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async startRecording(options) {
|
|
186
|
+
// Check if recording is already active
|
|
187
|
+
if (this.isRecordingActive()) {
|
|
188
|
+
throw new Error('Recording already in progress');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
// Import and call the recording function directly
|
|
193
|
+
const { startRecording } = await import('./recorder.js');
|
|
194
|
+
|
|
195
|
+
const recordingOptions = {
|
|
196
|
+
fps: parseInt(options.fps) || 10,
|
|
197
|
+
includeAudio: options.audio || false,
|
|
198
|
+
customOutputPath: options.output || null
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
logger.info('Starting recording directly', { options: recordingOptions });
|
|
202
|
+
|
|
203
|
+
const result = await startRecording(recordingOptions);
|
|
204
|
+
|
|
205
|
+
// Write status to track the recording
|
|
206
|
+
this.writePid(process.pid);
|
|
207
|
+
this.writeStatus({
|
|
208
|
+
isRecording: true,
|
|
209
|
+
startTime: result.startTime, // Use actual recording start time from recorder
|
|
210
|
+
options,
|
|
211
|
+
pid: process.pid,
|
|
212
|
+
outputPath: result.outputPath
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
logger.info('Recording started successfully', {
|
|
216
|
+
outputPath: result.outputPath,
|
|
217
|
+
startTime: result.startTime
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
pid: process.pid,
|
|
222
|
+
outputPath: result.outputPath,
|
|
223
|
+
startTime: result.startTime
|
|
224
|
+
};
|
|
225
|
+
} catch (error) {
|
|
226
|
+
logger.error('Failed to start recording', { error });
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async gracefulExit() {
|
|
232
|
+
logger.info('Graceful exit requested');
|
|
233
|
+
|
|
234
|
+
// If we're currently recording, stop it properly
|
|
235
|
+
if (this.isRecordingActive()) {
|
|
236
|
+
try {
|
|
237
|
+
logger.info('Stopping active recording before exit');
|
|
238
|
+
await this.stopActiveRecording();
|
|
239
|
+
logger.info('Recording stopped successfully during graceful exit');
|
|
240
|
+
} catch (error) {
|
|
241
|
+
logger.error('Failed to stop recording during graceful exit', { error });
|
|
242
|
+
this.cleanup(); // Fallback cleanup
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
// Just cleanup if no recording is active
|
|
246
|
+
this.cleanup();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const processManager = new ProcessManager();
|
|
254
|
+
|
|
255
|
+
export { processManager };
|