dashcam 0.8.4 → 1.0.1-beta.10

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.
Files changed (55) hide show
  1. package/.dashcam/cli-config.json +3 -0
  2. package/.dashcam/recording.log +135 -0
  3. package/.dashcam/web-config.json +11 -0
  4. package/.github/RELEASE.md +59 -0
  5. package/.github/workflows/publish.yml +43 -0
  6. package/BACKWARD_COMPATIBILITY.md +177 -0
  7. package/LOG_TRACKING_GUIDE.md +225 -0
  8. package/README.md +709 -155
  9. package/bin/dashcam-background.js +177 -0
  10. package/bin/dashcam.cjs +8 -0
  11. package/bin/dashcam.js +696 -0
  12. package/bin/index.js +63 -0
  13. package/examples/execute-script.js +152 -0
  14. package/examples/simple-test.js +37 -0
  15. package/lib/applicationTracker.js +311 -0
  16. package/lib/auth.js +222 -0
  17. package/lib/binaries.js +21 -0
  18. package/lib/config.js +34 -0
  19. package/lib/extension-logs/helpers.js +182 -0
  20. package/lib/extension-logs/index.js +347 -0
  21. package/lib/extension-logs/manager.js +344 -0
  22. package/lib/ffmpeg.js +155 -0
  23. package/lib/logTracker.js +23 -0
  24. package/lib/logger.js +118 -0
  25. package/lib/logs/index.js +488 -0
  26. package/lib/permissions.js +85 -0
  27. package/lib/processManager.js +317 -0
  28. package/lib/recorder.js +690 -0
  29. package/lib/store.js +58 -0
  30. package/lib/tracking/FileTracker.js +105 -0
  31. package/lib/tracking/FileTrackerManager.js +62 -0
  32. package/lib/tracking/LogsTracker.js +161 -0
  33. package/lib/tracking/active-win.js +212 -0
  34. package/lib/tracking/icons/darwin.js +39 -0
  35. package/lib/tracking/icons/index.js +167 -0
  36. package/lib/tracking/icons/windows.js +27 -0
  37. package/lib/tracking/idle.js +82 -0
  38. package/lib/tracking.js +23 -0
  39. package/lib/uploader.js +456 -0
  40. package/lib/utilities/jsonl.js +77 -0
  41. package/lib/webLogsDaemon.js +234 -0
  42. package/lib/websocket/server.js +223 -0
  43. package/package.json +53 -21
  44. package/recording.log +814 -0
  45. package/sea-bundle.mjs +34595 -0
  46. package/test-page.html +15 -0
  47. package/test.log +1 -0
  48. package/test_run.log +48 -0
  49. package/test_workflow.sh +154 -0
  50. package/examples/crash-test.js +0 -11
  51. package/examples/github-issue.sh +0 -1
  52. package/examples/protocol.html +0 -22
  53. package/index.js +0 -158
  54. package/lib.js +0 -199
  55. package/recorder.js +0 -85
@@ -0,0 +1,317 @@
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
164
+ const maxWaitTime = 30000; // 30 seconds max
165
+ const startWait = Date.now();
166
+
167
+ while (this.isProcessRunning(pid) && (Date.now() - startWait) < maxWaitTime) {
168
+ await new Promise(resolve => setTimeout(resolve, 500));
169
+ }
170
+
171
+ if (this.isProcessRunning(pid)) {
172
+ logger.warn('Process did not stop within timeout, forcing termination');
173
+ process.kill(pid, 'SIGKILL');
174
+ await new Promise(resolve => setTimeout(resolve, 1000));
175
+ }
176
+
177
+ // Call the actual recorder's stopRecording function to get complete results
178
+ if (status) {
179
+ try {
180
+ const { stopRecording } = await import('./recorder.js');
181
+ const result = await stopRecording();
182
+
183
+ logger.info('Recording stopped successfully via recorder', {
184
+ outputPath: result.outputPath,
185
+ duration: result.duration,
186
+ hasLogs: result.logs?.length > 0,
187
+ hasApps: result.apps?.length > 0
188
+ });
189
+
190
+ // Cleanup process files but preserve upload result for stop command
191
+ this.cleanup({ preserveResult: true });
192
+
193
+ return result;
194
+ } catch (recorderError) {
195
+ logger.warn('Failed to stop via recorder, falling back to basic result', { error: recorderError.message });
196
+
197
+ // Fallback to basic result if recorder fails
198
+ const basePath = status.outputPath.substring(0, status.outputPath.lastIndexOf('.'));
199
+ const result = {
200
+ outputPath: status.outputPath,
201
+ gifPath: `${basePath}.gif`,
202
+ snapshotPath: `${basePath}.png`,
203
+ duration: Date.now() - status.startTime,
204
+ clientStartDate: status.startTime,
205
+ apps: [],
206
+ logs: []
207
+ };
208
+
209
+ this.cleanup({ preserveResult: true });
210
+ return result;
211
+ }
212
+ } else {
213
+ throw new Error('No status information available for active recording');
214
+ }
215
+ } catch (error) {
216
+ logger.error('Failed to stop recording', { error });
217
+ throw error;
218
+ } finally {
219
+ this.isStopping = false;
220
+ }
221
+ }
222
+
223
+ async startRecording(options) {
224
+ // Check if recording is already active
225
+ if (this.isRecordingActive()) {
226
+ throw new Error('Recording already in progress');
227
+ }
228
+
229
+ // Always run in background mode by spawning a detached process
230
+ logger.info('Starting recording in background mode');
231
+
232
+ // Get the path to the CLI binary
233
+ const binPath = path.join(__dirname, '..', 'bin', 'dashcam-background.js');
234
+
235
+ logger.debug('Background process path', { binPath, exists: fs.existsSync(binPath) });
236
+
237
+ // Create log files for background process
238
+ const logDir = PROCESS_DIR;
239
+ const stdoutLog = path.join(logDir, 'background-stdout.log');
240
+ const stderrLog = path.join(logDir, 'background-stderr.log');
241
+
242
+ const stdoutFd = fs.openSync(stdoutLog, 'a');
243
+ const stderrFd = fs.openSync(stderrLog, 'a');
244
+
245
+ // Spawn a detached process that will handle the recording
246
+ const backgroundProcess = spawn(process.execPath, [
247
+ binPath,
248
+ JSON.stringify(options)
249
+ ], {
250
+ detached: true,
251
+ stdio: ['ignore', stdoutFd, stderrFd], // Log stdout and stderr
252
+ env: {
253
+ ...process.env,
254
+ DASHCAM_BACKGROUND: 'true'
255
+ }
256
+ });
257
+
258
+ // Close the file descriptors in the parent process
259
+ fs.closeSync(stdoutFd);
260
+ fs.closeSync(stderrFd);
261
+
262
+ // Get the background process PID before unreffing
263
+ const backgroundPid = backgroundProcess.pid;
264
+
265
+ // Allow the parent process to exit independently
266
+ backgroundProcess.unref();
267
+
268
+ // Wait a moment for the background process to initialize
269
+ await new Promise(resolve => setTimeout(resolve, 2000));
270
+
271
+ // Read the status file to get recording details
272
+ const status = this.readStatus();
273
+
274
+ if (!status || !status.isRecording) {
275
+ throw new Error('Background process failed to start recording');
276
+ }
277
+
278
+ // Write PID file so other commands can find the background process
279
+ this.writePid(status.pid);
280
+
281
+ logger.info('Background recording process started', {
282
+ pid: status.pid,
283
+ outputPath: status.outputPath
284
+ });
285
+
286
+ return {
287
+ pid: status.pid,
288
+ outputPath: status.outputPath,
289
+ startTime: status.startTime
290
+ };
291
+ }
292
+
293
+ async gracefulExit() {
294
+ logger.info('Graceful exit requested');
295
+
296
+ // If we're currently recording, stop it properly
297
+ if (this.isRecordingActive()) {
298
+ try {
299
+ logger.info('Stopping active recording before exit');
300
+ await this.stopActiveRecording();
301
+ logger.info('Recording stopped successfully during graceful exit');
302
+ } catch (error) {
303
+ logger.error('Failed to stop recording during graceful exit', { error });
304
+ this.cleanup(); // Fallback cleanup
305
+ }
306
+ } else {
307
+ // Just cleanup if no recording is active
308
+ this.cleanup();
309
+ }
310
+
311
+ process.exit(0);
312
+ }
313
+ }
314
+
315
+ const processManager = new ProcessManager();
316
+
317
+ export { processManager };