dashcam 0.8.3 → 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.
- 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/publish.yml +43 -0
- package/BACKWARD_COMPATIBILITY.md +177 -0
- package/LOG_TRACKING_GUIDE.md +225 -0
- package/README.md +709 -155
- package/bin/dashcam-background.js +177 -0
- package/bin/dashcam.cjs +8 -0
- package/bin/dashcam.js +696 -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 +155 -0
- package/lib/logTracker.js +23 -0
- package/lib/logger.js +118 -0
- package/lib/logs/index.js +488 -0
- package/lib/permissions.js +85 -0
- package/lib/processManager.js +317 -0
- package/lib/recorder.js +690 -0
- package/lib/store.js +58 -0
- package/lib/tracking/FileTracker.js +105 -0
- package/lib/tracking/FileTrackerManager.js +62 -0
- package/lib/tracking/LogsTracker.js +161 -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 +456 -0
- package/lib/utilities/jsonl.js +77 -0
- package/lib/webLogsDaemon.js +234 -0
- package/lib/websocket/server.js +223 -0
- package/package.json +53 -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 +154 -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 -177
- package/lib.js +0 -199
- package/recorder.js +0 -85
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
import { LogsTracker } from '../tracking/LogsTracker.js';
|
|
5
|
+
import { FileTrackerManager } from '../tracking/FileTrackerManager.js';
|
|
6
|
+
import { WebLogsDaemon, DAEMON_CONFIG_FILE } from '../webLogsDaemon.js';
|
|
7
|
+
import { WebLogsTracker } from '../extension-logs/index.js';
|
|
8
|
+
import { WebTrackerManager } from '../extension-logs/manager.js';
|
|
9
|
+
import { server } from '../websocket/server.js';
|
|
10
|
+
import { jsonl } from '../utilities/jsonl.js';
|
|
11
|
+
import { filterWebEvents } from '../extension-logs/helpers.js';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
|
|
14
|
+
const CLI_CONFIG_FILE = path.join(process.cwd(), '.dashcam', 'cli-config.json');
|
|
15
|
+
|
|
16
|
+
// Simple trim function for CLI (adapted from desktop app)
|
|
17
|
+
async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipId) {
|
|
18
|
+
logger.info('Trimming logs', {
|
|
19
|
+
count: groupLogStatuses.length,
|
|
20
|
+
startMS,
|
|
21
|
+
endMS,
|
|
22
|
+
clientStartDate,
|
|
23
|
+
clientStartDateReadable: new Date(clientStartDate).toISOString()
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const REPLAY_DIR = path.join(os.tmpdir(), 'dashcam', 'recordings');
|
|
27
|
+
|
|
28
|
+
// Filter out logs with no content
|
|
29
|
+
groupLogStatuses = groupLogStatuses.filter((status) => status.count);
|
|
30
|
+
|
|
31
|
+
let webHandled = false;
|
|
32
|
+
|
|
33
|
+
groupLogStatuses.forEach((status) => {
|
|
34
|
+
if (!status.fileLocation || !status.count) return;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const parsed = path.parse(status.fileLocation);
|
|
38
|
+
const content = jsonl.read(status.fileLocation);
|
|
39
|
+
|
|
40
|
+
if (!content || !Array.isArray(content)) return;
|
|
41
|
+
|
|
42
|
+
let events = content;
|
|
43
|
+
|
|
44
|
+
// Convert events to relative time
|
|
45
|
+
let relativeEvents = events.map((event, index) => {
|
|
46
|
+
const originalTime = event.time;
|
|
47
|
+
event.time = parseInt(event.time + '') - startMS;
|
|
48
|
+
// Check if it's not already relative time
|
|
49
|
+
if (event.time > 1_000_000_000_000) {
|
|
50
|
+
// relative time = absolute time - clip start time
|
|
51
|
+
event.time = event.time - clientStartDate;
|
|
52
|
+
if (index === 0) {
|
|
53
|
+
// Log first event for debugging
|
|
54
|
+
logger.info('First event timestamp conversion', {
|
|
55
|
+
originalTime,
|
|
56
|
+
clientStartDate,
|
|
57
|
+
relativeTime: event.time,
|
|
58
|
+
relativeSeconds: (event.time / 1000).toFixed(2),
|
|
59
|
+
startMS,
|
|
60
|
+
line: event.line ? event.line.substring(0, 50) : 'N/A'
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return event;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const duration = endMS - startMS;
|
|
68
|
+
let filteredEvents = relativeEvents;
|
|
69
|
+
|
|
70
|
+
if (status.type === 'application' || status.type === 'cli') {
|
|
71
|
+
// Filter events within the time range
|
|
72
|
+
filteredEvents = filteredEvents.filter((event) => {
|
|
73
|
+
return event.time >= 0 && event.time <= duration;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (status.type === 'cli') {
|
|
77
|
+
// For CLI logs, keep the file paths in logFile field for UI display
|
|
78
|
+
// No need to remap to numeric indices
|
|
79
|
+
|
|
80
|
+
// Create items array showing unique files and their event counts
|
|
81
|
+
const fileCounts = {};
|
|
82
|
+
filteredEvents.forEach(event => {
|
|
83
|
+
fileCounts[event.logFile] = (fileCounts[event.logFile] || 0) + 1;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
status.items = Object.entries(fileCounts).map(([filePath, count]) => ({
|
|
87
|
+
item: filePath,
|
|
88
|
+
count
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
} else if (status.type === 'web' && !webHandled) {
|
|
92
|
+
logger.debug('Found web groupLog, handling all web groupLogs at once');
|
|
93
|
+
// We do this because weblogs have a single shared jsonl file
|
|
94
|
+
// shared between all web logs
|
|
95
|
+
filteredEvents = filterWebEvents(
|
|
96
|
+
filteredEvents,
|
|
97
|
+
groupLogStatuses.filter((status) => status.type === 'web'),
|
|
98
|
+
0,
|
|
99
|
+
duration
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
webHandled = true;
|
|
103
|
+
} else if (status.type === 'web') {
|
|
104
|
+
// Skip processing for additional web logs - already handled
|
|
105
|
+
status.trimmedFileLocation = path.join(
|
|
106
|
+
REPLAY_DIR,
|
|
107
|
+
[clipId, parsed.base].join('_')
|
|
108
|
+
);
|
|
109
|
+
status.count = filteredEvents.length;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
logger.debug('Filtered events', {
|
|
114
|
+
source: events.length,
|
|
115
|
+
filtered: filteredEvents.length,
|
|
116
|
+
difference: events.length - filteredEvents.length,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
status.count = filteredEvents.length;
|
|
120
|
+
status.trimmedFileLocation = jsonl.write(
|
|
121
|
+
REPLAY_DIR,
|
|
122
|
+
[clipId, parsed.base].join('_'),
|
|
123
|
+
filteredEvents
|
|
124
|
+
);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logger.error('Error trimming log file', { file: status.fileLocation, error });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Handle shared web log file location
|
|
131
|
+
const firstWebLog = groupLogStatuses.find(
|
|
132
|
+
(status) => status.type === 'web' && status.trimmedFileLocation
|
|
133
|
+
);
|
|
134
|
+
if (firstWebLog) {
|
|
135
|
+
groupLogStatuses
|
|
136
|
+
.filter((status) => status.type === 'web')
|
|
137
|
+
.forEach(
|
|
138
|
+
(status) =>
|
|
139
|
+
(status.trimmedFileLocation = firstWebLog.trimmedFileLocation)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return groupLogStatuses;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
class LogsTrackerManager {
|
|
147
|
+
constructor() {
|
|
148
|
+
this.instances = {};
|
|
149
|
+
this.cliConfig = {};
|
|
150
|
+
this.webLogsConfig = {};
|
|
151
|
+
this.fileTrackerManager = new FileTrackerManager();
|
|
152
|
+
|
|
153
|
+
// Load persisted configs
|
|
154
|
+
this.loadCliConfig();
|
|
155
|
+
this.loadWebConfig();
|
|
156
|
+
|
|
157
|
+
// Create the singleton watch-only tracker for CLI files
|
|
158
|
+
this.watchTracker = new LogsTracker({
|
|
159
|
+
config: this.cliConfig,
|
|
160
|
+
fileTrackerManager: this.fileTrackerManager
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async ensureWebDaemonRunning() {
|
|
165
|
+
try {
|
|
166
|
+
await WebLogsDaemon.ensureDaemonRunning();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.error('Failed to ensure web daemon is running', { error });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
loadWebConfig() {
|
|
173
|
+
try {
|
|
174
|
+
if (fs.existsSync(DAEMON_CONFIG_FILE)) {
|
|
175
|
+
const data = fs.readFileSync(DAEMON_CONFIG_FILE, 'utf8');
|
|
176
|
+
this.webLogsConfig = JSON.parse(data);
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
logger.error('Failed to load web config', { error });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
saveWebConfig() {
|
|
184
|
+
try {
|
|
185
|
+
fs.writeFileSync(DAEMON_CONFIG_FILE, JSON.stringify(this.webLogsConfig, null, 2));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.error('Failed to save web config', { error });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
loadCliConfig() {
|
|
192
|
+
try {
|
|
193
|
+
if (fs.existsSync(CLI_CONFIG_FILE)) {
|
|
194
|
+
const data = fs.readFileSync(CLI_CONFIG_FILE, 'utf8');
|
|
195
|
+
this.cliConfig = JSON.parse(data);
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger.error('Failed to load CLI config', { error });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
saveCliConfig() {
|
|
203
|
+
try {
|
|
204
|
+
// Ensure directory exists
|
|
205
|
+
const dir = path.dirname(CLI_CONFIG_FILE);
|
|
206
|
+
if (!fs.existsSync(dir)) {
|
|
207
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
208
|
+
}
|
|
209
|
+
fs.writeFileSync(CLI_CONFIG_FILE, JSON.stringify(this.cliConfig, null, 2));
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logger.error('Failed to save CLI config', { error });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
updateLogsConfig(config) {
|
|
216
|
+
// Update web logs config
|
|
217
|
+
const webConfigs = Array.isArray(config) ?
|
|
218
|
+
config.filter(app => app.type === 'web' && app.enabled === true)
|
|
219
|
+
.reduce((map, config) => {
|
|
220
|
+
map[config.id] = config;
|
|
221
|
+
return map;
|
|
222
|
+
}, {}) : {};
|
|
223
|
+
|
|
224
|
+
this.webLogsConfig = webConfigs;
|
|
225
|
+
this.saveWebConfig();
|
|
226
|
+
|
|
227
|
+
logger.info('Updated logs config', { webConfigs });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async addWebTracker(config) {
|
|
231
|
+
this.webLogsConfig[config.id] = config;
|
|
232
|
+
this.saveWebConfig();
|
|
233
|
+
|
|
234
|
+
// Ensure daemon is running and will pick up the new config
|
|
235
|
+
await this.ensureWebDaemonRunning();
|
|
236
|
+
|
|
237
|
+
logger.info(`Added web tracker: ${config.name}`, { patterns: config.patterns });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
pushCliTrackedPath(filePath) {
|
|
241
|
+
if (!this.cliConfig[filePath]) {
|
|
242
|
+
this.cliConfig[filePath] = true;
|
|
243
|
+
this.watchTracker.updateConfig(this.cliConfig);
|
|
244
|
+
this.saveCliConfig();
|
|
245
|
+
logger.info(`Added CLI tracked path: ${filePath}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
removeCliTrackedPath(filePath) {
|
|
250
|
+
if (this.cliConfig[filePath]) {
|
|
251
|
+
delete this.cliConfig[filePath];
|
|
252
|
+
this.watchTracker.updateConfig(this.cliConfig);
|
|
253
|
+
this.saveCliConfig();
|
|
254
|
+
logger.info(`Removed CLI tracked path: ${filePath}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// CLI interface methods
|
|
259
|
+
addCliLogFile(filePath) {
|
|
260
|
+
this.pushCliTrackedPath(filePath);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
removeCliLogFile(filePath) {
|
|
264
|
+
this.removeCliTrackedPath(filePath);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
addPipedLog(content) {
|
|
268
|
+
// Find active recording instance
|
|
269
|
+
const activeInstances = Object.values(this.instances);
|
|
270
|
+
if (activeInstances.length === 0) {
|
|
271
|
+
logger.warn('No active recording to add piped content to');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Add to the most recent active instance (last one)
|
|
276
|
+
const instance = activeInstances[activeInstances.length - 1];
|
|
277
|
+
const timestamp = Date.now();
|
|
278
|
+
|
|
279
|
+
// Write to CLI tracker's temp file
|
|
280
|
+
if (instance.trackers && instance.trackers.cli) {
|
|
281
|
+
const pipedLog = {
|
|
282
|
+
time: timestamp,
|
|
283
|
+
logFile: 'piped-input',
|
|
284
|
+
content: content
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Write to the CLI tracker's output file
|
|
288
|
+
try {
|
|
289
|
+
const outputFile = path.join(instance.directory, 'cli-logs.jsonl');
|
|
290
|
+
const logLine = JSON.stringify(pipedLog) + '\n';
|
|
291
|
+
fs.appendFileSync(outputFile, logLine);
|
|
292
|
+
logger.info('Added piped content to recording', {
|
|
293
|
+
recorderId: instance.recorderId,
|
|
294
|
+
contentLength: content.length
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
logger.error('Failed to write piped content', { error });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
removeTracker(id) {
|
|
303
|
+
// Try removing from web trackers first
|
|
304
|
+
if (this.webLogsConfig[id]) {
|
|
305
|
+
delete this.webLogsConfig[id];
|
|
306
|
+
this.saveWebConfig();
|
|
307
|
+
logger.info(`Removed web tracker: ${id}`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check if it's a file tracker (format: file-1, file-2, etc.)
|
|
312
|
+
if (id.startsWith('file-')) {
|
|
313
|
+
const fileIndex = parseInt(id.split('-')[1]) - 1;
|
|
314
|
+
const cliFiles = Object.keys(this.cliConfig);
|
|
315
|
+
if (fileIndex >= 0 && fileIndex < cliFiles.length) {
|
|
316
|
+
const filePath = cliFiles[fileIndex];
|
|
317
|
+
this.removeCliTrackedPath(filePath);
|
|
318
|
+
logger.info(`Removed file tracker: ${filePath}`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
logger.warn(`Tracker not found: ${id}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
getStatus() {
|
|
327
|
+
// Load current web config
|
|
328
|
+
this.loadWebConfig();
|
|
329
|
+
|
|
330
|
+
const activeInstances = Object.keys(this.instances).length;
|
|
331
|
+
const cliFilesCount = Object.keys(this.cliConfig).length;
|
|
332
|
+
const webAppsCount = Object.keys(this.webLogsConfig).length;
|
|
333
|
+
|
|
334
|
+
// Get file tracker stats
|
|
335
|
+
const fileTrackerStats = Object.keys(this.cliConfig).map(filePath => {
|
|
336
|
+
const stats = this.fileTrackerManager.getStats(filePath);
|
|
337
|
+
return {
|
|
338
|
+
filePath,
|
|
339
|
+
count: stats.count
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const totalEvents = fileTrackerStats.reduce((sum, stat) => sum + stat.count, 0);
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
activeInstances,
|
|
347
|
+
cliFilesCount,
|
|
348
|
+
webAppsCount,
|
|
349
|
+
totalEvents,
|
|
350
|
+
fileTrackerStats,
|
|
351
|
+
cliFiles: Object.keys(this.cliConfig),
|
|
352
|
+
webApps: Object.values(this.webLogsConfig).map(config => ({
|
|
353
|
+
id: config.id,
|
|
354
|
+
name: config.name,
|
|
355
|
+
patterns: config.patterns
|
|
356
|
+
})),
|
|
357
|
+
webDaemonRunning: WebLogsDaemon.isDaemonRunning()
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async startNew({ recorderId, screenId, directory }) {
|
|
362
|
+
logger.debug('LogsTrackerManager: Starting new logs tracker instance', { recorderId, screenId, directory });
|
|
363
|
+
|
|
364
|
+
const instanceKey = `${recorderId}_${screenId}`;
|
|
365
|
+
|
|
366
|
+
// Create recording tracker for CLI logs
|
|
367
|
+
const cliTracker = new LogsTracker({
|
|
368
|
+
directory,
|
|
369
|
+
config: { ...this.cliConfig }, // Copy current config
|
|
370
|
+
fileTrackerManager: this.fileTrackerManager,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Start WebSocket server if not already running
|
|
374
|
+
if (!server.isListening.value) {
|
|
375
|
+
logger.debug('LogsTrackerManager: Starting WebSocket server...');
|
|
376
|
+
await server.start();
|
|
377
|
+
logger.info('LogsTrackerManager: WebSocket server started on port', { port: server.port });
|
|
378
|
+
} else {
|
|
379
|
+
logger.debug('LogsTrackerManager: WebSocket server already running on port', { port: server.port });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Create a WebTrackerManager instance for this recording
|
|
383
|
+
logger.debug('LogsTrackerManager: Creating WebTrackerManager for recording...');
|
|
384
|
+
const webTrackerManager = new WebTrackerManager(server);
|
|
385
|
+
|
|
386
|
+
// Create recording tracker for web logs with directory to write events to file
|
|
387
|
+
logger.debug('LogsTrackerManager: Creating WebLogsTracker for recording...', {
|
|
388
|
+
directory,
|
|
389
|
+
webConfigCount: Object.keys(this.webLogsConfig).length
|
|
390
|
+
});
|
|
391
|
+
const webTracker = new WebLogsTracker({
|
|
392
|
+
config: { ...this.webLogsConfig }, // Copy current web config
|
|
393
|
+
webTrackerManager,
|
|
394
|
+
directory // This makes it NOT watch-only, so events will be written to file
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
this.instances[instanceKey] = {
|
|
398
|
+
recorderId,
|
|
399
|
+
screenId,
|
|
400
|
+
directory,
|
|
401
|
+
trackers: {
|
|
402
|
+
cli: cliTracker,
|
|
403
|
+
web: webTracker,
|
|
404
|
+
webTrackerManager // Store this so we can clean it up later
|
|
405
|
+
},
|
|
406
|
+
startTime: Date.now(),
|
|
407
|
+
endTime: undefined,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
logger.info(`Started new logs tracker instance with web support`, {
|
|
411
|
+
recorderId,
|
|
412
|
+
screenId,
|
|
413
|
+
directory,
|
|
414
|
+
webConfigCount: Object.keys(this.webLogsConfig).length
|
|
415
|
+
});
|
|
416
|
+
return this.instances[instanceKey];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async stop({ recorderId, screenId }) {
|
|
420
|
+
const instanceKey = `${recorderId}_${screenId}`;
|
|
421
|
+
const instance = this.instances[instanceKey];
|
|
422
|
+
|
|
423
|
+
if (!instance) {
|
|
424
|
+
logger.warn(`No logs tracker instance found for ${instanceKey}`);
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
delete this.instances[instanceKey];
|
|
429
|
+
|
|
430
|
+
// Stop CLI tracker
|
|
431
|
+
const cliStatus = instance.trackers.cli.destroy();
|
|
432
|
+
|
|
433
|
+
// Stop web tracker if it exists
|
|
434
|
+
let webStatus = [];
|
|
435
|
+
if (instance.trackers.web) {
|
|
436
|
+
logger.debug('LogsTrackerManager: Stopping web tracker...', { recorderId, screenId });
|
|
437
|
+
webStatus = instance.trackers.web.destroy();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Clean up WebTrackerManager if it exists
|
|
441
|
+
if (instance.trackers.webTrackerManager) {
|
|
442
|
+
logger.debug('LogsTrackerManager: Destroying WebTrackerManager...', { recorderId, screenId });
|
|
443
|
+
instance.trackers.webTrackerManager.destroy();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Stop WebSocket server if no more recording instances are active
|
|
447
|
+
const remainingInstances = Object.keys(this.instances).length;
|
|
448
|
+
if (remainingInstances === 0 && server.isListening.value) {
|
|
449
|
+
logger.debug('LogsTrackerManager: No more recording instances, stopping WebSocket server...');
|
|
450
|
+
await server.stop();
|
|
451
|
+
logger.info('LogsTrackerManager: WebSocket server stopped');
|
|
452
|
+
} else {
|
|
453
|
+
logger.debug('LogsTrackerManager: WebSocket server kept running', { remainingInstances });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
logger.info(`Stopped logs tracker instance with web support`, {
|
|
457
|
+
recorderId,
|
|
458
|
+
screenId,
|
|
459
|
+
cliStatusCount: cliStatus.length,
|
|
460
|
+
webStatusCount: webStatus.length
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Combine CLI and web statuses
|
|
464
|
+
return [...cliStatus, ...webStatus];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
stopAll() {
|
|
468
|
+
logger.info('Stopping all logs tracker instances');
|
|
469
|
+
const results = [];
|
|
470
|
+
for (const instanceKey of Object.keys(this.instances)) {
|
|
471
|
+
const [recorderId, screenId] = instanceKey.split('_');
|
|
472
|
+
results.push(...this.stop({ recorderId, screenId }));
|
|
473
|
+
}
|
|
474
|
+
return results;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
destroy() {
|
|
478
|
+
this.stopAll();
|
|
479
|
+
this.watchTracker.destroy();
|
|
480
|
+
this.fileTrackerManager.destroy();
|
|
481
|
+
// Note: Don't stop the web daemon here as it should persist
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Create singleton instance
|
|
486
|
+
const logsTrackerManager = new LogsTrackerManager();
|
|
487
|
+
|
|
488
|
+
export { trimLogs, logsTrackerManager };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if the application has screen recording permissions on macOS
|
|
7
|
+
*/
|
|
8
|
+
async function checkScreenRecordingPermission() {
|
|
9
|
+
const platform = os.platform();
|
|
10
|
+
|
|
11
|
+
if (platform !== 'darwin') {
|
|
12
|
+
// Not macOS, permissions check not needed
|
|
13
|
+
return { hasPermission: true, platform };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Try to capture a single frame to test permissions
|
|
18
|
+
// This is a quick test that will fail immediately if permissions are denied
|
|
19
|
+
const { stderr } = await execa('screencapture', ['-x', '-t', 'png', '/tmp/dashcam_permission_test.png'], {
|
|
20
|
+
timeout: 2000,
|
|
21
|
+
reject: false
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Clean up test file
|
|
25
|
+
try {
|
|
26
|
+
const fs = await import('fs');
|
|
27
|
+
if (fs.existsSync('/tmp/dashcam_permission_test.png')) {
|
|
28
|
+
fs.unlinkSync('/tmp/dashcam_permission_test.png');
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// Ignore cleanup errors
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const hasPermission = !stderr || !stderr.includes('not permitted');
|
|
35
|
+
|
|
36
|
+
return { hasPermission, platform: 'darwin' };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logger.debug('Permission check failed', { error: error.message });
|
|
39
|
+
return { hasPermission: false, platform: 'darwin', error: error.message };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Display instructions for granting screen recording permissions
|
|
45
|
+
*/
|
|
46
|
+
function showPermissionInstructions() {
|
|
47
|
+
const platform = os.platform();
|
|
48
|
+
|
|
49
|
+
if (platform !== 'darwin') {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('\n⚠️ Screen Recording Permission Required\n');
|
|
54
|
+
console.log('Dashcam needs screen recording permission to capture your screen.');
|
|
55
|
+
console.log('\nTo grant permission:');
|
|
56
|
+
console.log('1. Open System Settings (or System Preferences)');
|
|
57
|
+
console.log('2. Go to Privacy & Security > Screen Recording');
|
|
58
|
+
console.log('3. Click the lock icon and enter your password');
|
|
59
|
+
console.log('4. Enable screen recording for Terminal (or your terminal app)');
|
|
60
|
+
console.log(' - If using the standalone binary, you may need to add the binary itself');
|
|
61
|
+
console.log('5. Restart your terminal and try again\n');
|
|
62
|
+
console.log('Note: You may need to fully quit and restart your terminal application.\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check permissions and show instructions if needed
|
|
67
|
+
*/
|
|
68
|
+
async function ensurePermissions() {
|
|
69
|
+
const result = await checkScreenRecordingPermission();
|
|
70
|
+
|
|
71
|
+
if (!result.hasPermission) {
|
|
72
|
+
logger.warn('Screen recording permission not granted');
|
|
73
|
+
showPermissionInstructions();
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
logger.debug('Screen recording permission check passed');
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
checkScreenRecordingPermission,
|
|
83
|
+
showPermissionInstructions,
|
|
84
|
+
ensurePermissions
|
|
85
|
+
};
|