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
package/lib/store.js ADDED
@@ -0,0 +1,58 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { APP } from './config.js';
4
+ import { logger } from './logger.js';
5
+
6
+ class Store {
7
+ constructor(filename) {
8
+ this.path = path.join(APP.configDir, `${filename}.json`);
9
+ this.data = this.load();
10
+ }
11
+
12
+ load() {
13
+ try {
14
+ if (fs.existsSync(this.path)) {
15
+ const data = fs.readFileSync(this.path, 'utf8');
16
+ return JSON.parse(data);
17
+ }
18
+ } catch (error) {
19
+ logger.error('Failed to load store:', error);
20
+ }
21
+ return {};
22
+ }
23
+
24
+ save() {
25
+ try {
26
+ // Ensure directory exists
27
+ fs.mkdirSync(path.dirname(this.path), { recursive: true });
28
+ fs.writeFileSync(this.path, JSON.stringify(this.data, null, 2));
29
+ } catch (error) {
30
+ logger.error('Failed to save store:', error);
31
+ }
32
+ }
33
+
34
+ get(key) {
35
+ return this.data[key];
36
+ }
37
+
38
+ set(key, value) {
39
+ this.data[key] = value;
40
+ this.save();
41
+ }
42
+
43
+ delete(key) {
44
+ delete this.data[key];
45
+ this.save();
46
+ }
47
+
48
+ has(key) {
49
+ return key in this.data;
50
+ }
51
+
52
+ clear() {
53
+ this.data = {};
54
+ this.save();
55
+ }
56
+ }
57
+
58
+ export { Store };
@@ -0,0 +1,105 @@
1
+ import { Tail } from 'tail';
2
+ import { logger } from '../logger.js';
3
+ import fs from 'fs';
4
+
5
+ // Simple function to get stats for events in the last minute
6
+ function getStats(eventTimes = []) {
7
+ const endTime = Date.now();
8
+ const startTime = Date.now() - 60000;
9
+
10
+ let startIndex = 0;
11
+ let count = 0;
12
+
13
+ for (const time of eventTimes) {
14
+ if (time < startTime) startIndex++;
15
+ else if (time <= endTime) {
16
+ count++;
17
+ } else break;
18
+ }
19
+
20
+ return {
21
+ eventTimes: eventTimes.slice(startIndex),
22
+ count,
23
+ };
24
+ }
25
+
26
+ // Simple ANSI escape code regex for stripping colors
27
+ const ansiRegex = /[\u001B\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d\\/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))/g;
28
+
29
+ function stripAnsi(string) {
30
+ if (typeof string !== 'string') {
31
+ throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
32
+ }
33
+ return string.replace(ansiRegex, '');
34
+ }
35
+
36
+ export class FileTracker {
37
+ constructor(trackedFile, callback) {
38
+ this.tail = null;
39
+ this.eventTimes = [];
40
+ this.callback = callback;
41
+ this.trackedFile = trackedFile;
42
+
43
+ try {
44
+ // Ensure the file exists before creating the Tail watcher
45
+ if (!fs.existsSync(this.trackedFile)) {
46
+ logger.warn(`File does not exist, creating: ${this.trackedFile}`);
47
+ fs.writeFileSync(this.trackedFile, '', 'utf8');
48
+ }
49
+
50
+ this.tail = new Tail(this.trackedFile, { encoding: 'ascii' });
51
+ this.tail.on('line', (line) => {
52
+ const time = Date.now();
53
+ this.eventTimes.push(time);
54
+
55
+ // Log errors for debugging (simplified error handling)
56
+ if (line.toLowerCase().indexOf('error') > -1) {
57
+ logger.warn('Error found in log file', {
58
+ file: trackedFile,
59
+ line: stripAnsi(line).substring(0, 200)
60
+ });
61
+ }
62
+
63
+ if (!this.callback) return;
64
+
65
+ try {
66
+ this.callback({
67
+ line,
68
+ time,
69
+ logFile: this.trackedFile,
70
+ });
71
+ } catch (err) {
72
+ logger.error(
73
+ `FAILED callback for FileTracker ${this.trackedFile} with error:`,
74
+ err
75
+ );
76
+ }
77
+ });
78
+
79
+ this.tail.on('error', (data) => {
80
+ logger.error(
81
+ `Error in file tracker for file "${this.trackedFile}": ${data}`
82
+ );
83
+ });
84
+ } catch (e) {
85
+ logger.error('Failed to create FileTracker', { trackedFile, error: e });
86
+ }
87
+ }
88
+
89
+ destroy() {
90
+ if (this.tail) {
91
+ this.tail.unwatch();
92
+ this.tail = null;
93
+ }
94
+ }
95
+
96
+ getStats() {
97
+ const { eventTimes, count } = getStats(this.eventTimes);
98
+
99
+ this.eventTimes = eventTimes;
100
+ return {
101
+ count,
102
+ item: this.trackedFile,
103
+ };
104
+ }
105
+ }
@@ -0,0 +1,62 @@
1
+ import { logger } from '../logger.js';
2
+ import { FileTracker } from './FileTracker.js';
3
+
4
+ export class FileTrackerManager {
5
+ constructor() {
6
+ this.byFilePath = {};
7
+ }
8
+
9
+ getStats(filePath) {
10
+ if (!this.byFilePath[filePath]) {
11
+ return { item: filePath, count: 0 };
12
+ }
13
+ return this.byFilePath[filePath].tracker.getStats();
14
+ }
15
+
16
+ subscribe(filePath, callback) {
17
+ if (!this.byFilePath[filePath]) {
18
+ this.byFilePath[filePath] = {
19
+ callbacks: [],
20
+ tracker: new FileTracker(filePath, (event) => {
21
+ this.#sendEvent(filePath, event);
22
+ })
23
+ };
24
+ }
25
+ this.byFilePath[filePath].callbacks.push(callback);
26
+ return () => this.unsubscribe(filePath, callback);
27
+ }
28
+
29
+ unsubscribe(filePath, callback) {
30
+ if (!this.byFilePath[filePath]) return;
31
+
32
+ this.byFilePath[filePath].callbacks = this.byFilePath[filePath].callbacks
33
+ .filter(cb => cb !== callback);
34
+
35
+ if (this.byFilePath[filePath].callbacks.length === 0) {
36
+ this.byFilePath[filePath].tracker.destroy();
37
+ delete this.byFilePath[filePath];
38
+ }
39
+ }
40
+
41
+ #sendEvent(filePath, event) {
42
+ if (!this.byFilePath[filePath]) return;
43
+
44
+ for (const callback of this.byFilePath[filePath].callbacks) {
45
+ try {
46
+ callback(event);
47
+ } catch (error) {
48
+ logger.error(
49
+ "Failed sending FileTracker event",
50
+ { event, filePath, error }
51
+ );
52
+ }
53
+ }
54
+ }
55
+
56
+ destroy() {
57
+ for (const filePath of Object.keys(this.byFilePath)) {
58
+ this.byFilePath[filePath].tracker.destroy();
59
+ }
60
+ this.byFilePath = {};
61
+ }
62
+ }
@@ -0,0 +1,161 @@
1
+ import path from 'path';
2
+ import { logger } from '../logger.js';
3
+ import { FileTrackerManager } from './FileTrackerManager.js';
4
+ import { jsonl } from '../utilities/jsonl.js';
5
+
6
+ export class LogsTracker {
7
+ // Omitting a directory puts it in a watch only mode where it
8
+ // only collect per minute stats
9
+ constructor({ config = {}, directory, fileTrackerManager }) {
10
+ this.files = {};
11
+ this.fileIndex = 0;
12
+ this.fileToIndex = {};
13
+ this.config = config;
14
+ this.isWatchOnly = !directory;
15
+ this.fileTrackerManager = fileTrackerManager || new FileTrackerManager();
16
+
17
+ const filename = 'dashcam_logs_cli.jsonl';
18
+ this.fileLocation = this.isWatchOnly ? '' : path.join(directory, filename);
19
+
20
+ // Start tracking files from initial config
21
+ this._updateTrackedFiles();
22
+ }
23
+
24
+ updateConfig(config) {
25
+ this.config = config;
26
+ this._updateTrackedFiles();
27
+ }
28
+
29
+ _updateTrackedFiles() {
30
+ const updatedFilePaths = Object.keys(this.config);
31
+ updatedFilePaths.forEach((filePath) => this._startFileTracker(filePath));
32
+
33
+ Object.keys(this.files)
34
+ .filter((filePath) => !updatedFilePaths.includes(filePath))
35
+ .forEach((filePath) => this._stopFileTracker(filePath));
36
+ }
37
+
38
+ _startFileTracker(filePath) {
39
+ if (this.files[filePath]) return;
40
+
41
+ const index = ++this.fileIndex;
42
+ this.fileToIndex[filePath] = index;
43
+ const status = {
44
+ item: index,
45
+ count: 0,
46
+ };
47
+ const callback = (event) => {
48
+ if (!this.fileLocation) return;
49
+ jsonl.append(this.fileLocation, {
50
+ ...event,
51
+ logFile: filePath,
52
+ });
53
+ status.count++;
54
+ };
55
+
56
+ this.files[filePath] = {
57
+ status,
58
+ filePath, // Store file path for later reference
59
+ unsubscribe: this.fileTrackerManager.subscribe(filePath, callback),
60
+ };
61
+
62
+ logger.info(`Started tracking logs for ${filePath}`);
63
+ }
64
+
65
+ _stopFileTracker(filePath) {
66
+ const unsubscribe = this.files[filePath]?.unsubscribe;
67
+ if (unsubscribe) {
68
+ delete this.fileToIndex[filePath];
69
+ unsubscribe();
70
+ delete this.files[filePath];
71
+ logger.info(`Stopped tracking logs for ${filePath}`);
72
+ }
73
+ }
74
+
75
+ // Legacy methods for backwards compatibility
76
+ startTracking(filePath) {
77
+ if (!this.config[filePath]) {
78
+ this.config[filePath] = true;
79
+ this._updateTrackedFiles();
80
+ }
81
+ }
82
+
83
+ stopTracking(filePath) {
84
+ if (this.config[filePath]) {
85
+ delete this.config[filePath];
86
+ this._updateTrackedFiles();
87
+ }
88
+ }
89
+
90
+ startRecording(recordingPath) {
91
+ this.fileLocation = recordingPath;
92
+ logger.info(`Started recording to ${recordingPath}`);
93
+ }
94
+
95
+ stopRecording() {
96
+ this.fileLocation = null;
97
+ logger.info('Stopped recording');
98
+ }
99
+
100
+ getStatus() {
101
+ let items = [];
102
+ let filePathMap = {};
103
+
104
+ if (this.isWatchOnly) {
105
+ items = Object.keys(this.files).map((filePath) => {
106
+ const index = this.fileToIndex[filePath];
107
+ filePathMap[index] = filePath;
108
+ return {
109
+ ...this.fileTrackerManager.getStats(filePath),
110
+ item: index, // Keep numeric index to match events
111
+ };
112
+ });
113
+ } else {
114
+ items = Object.values(this.files).map(({ status, filePath }) => {
115
+ const index = status.item;
116
+ filePathMap[index] = filePath;
117
+ return {
118
+ ...status,
119
+ };
120
+ });
121
+ }
122
+
123
+ const totalCount = items.reduce((acc, status) => acc + status.count, 0);
124
+
125
+ return [
126
+ {
127
+ id: 'CLI',
128
+ name: 'File Logs', // More descriptive name
129
+ type: 'cli',
130
+ fileLocation: this.fileLocation,
131
+ items: items,
132
+ count: totalCount,
133
+ filePathMap: filePathMap, // Include mapping for UI to display file paths
134
+ },
135
+ ];
136
+ }
137
+
138
+ // Legacy method for backwards compatibility
139
+ getStats() {
140
+ const status = this.getStatus();
141
+ return status.length > 0 ? status[0] : {
142
+ id: 'CLI',
143
+ name: 'File Logs',
144
+ type: 'cli',
145
+ fileLocation: this.fileLocation,
146
+ items: [],
147
+ count: 0
148
+ };
149
+ }
150
+
151
+ destroy() {
152
+ const status = this.getStatus();
153
+ for (const filePath of Object.keys(this.files)) {
154
+ this._stopFileTracker(filePath);
155
+ }
156
+ this.fileTrackerManager.destroy();
157
+ this.fileLocation = null;
158
+ logger.info('Destroyed log tracker');
159
+ return status;
160
+ }
161
+ }
@@ -0,0 +1,212 @@
1
+ const _ = require("lodash");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const { logger, logFunctionCall } = require("../logger.js");
5
+ const { extractIcon } = require("./icons");
6
+
7
+ // Try to load get-windows, but handle gracefully if it's not available (e.g., in pkg builds)
8
+ let activeWindowSync = null;
9
+
10
+ try {
11
+ const getWindows = require("get-windows");
12
+ activeWindowSync = getWindows.activeWindowSync;
13
+ logger.debug("Successfully loaded get-windows module");
14
+ } catch (error) {
15
+ logger.debug("get-windows not available, application tracking will be disabled");
16
+ }
17
+
18
+ const normalizeAppName = (name) => {
19
+ name = name.split(".exe")[0];
20
+ name = name.toLowerCase();
21
+ return name;
22
+ };
23
+
24
+ // Simple JSONL utilities for CLI
25
+ const jsonl = {
26
+ append: (filePath, data) => {
27
+ const line = JSON.stringify(data) + '\n';
28
+ fs.appendFileSync(filePath, line);
29
+ }
30
+ };
31
+
32
+ class ActiveWin {
33
+ constructor({ recorderId, screenId, directory, fileName }) {
34
+ this.recorderId = recorderId;
35
+ this.screenId = screenId;
36
+ this.directory = directory;
37
+ this.fileName = fileName;
38
+
39
+ this.intervalRef = null;
40
+ this.failedAttempts = 0;
41
+ }
42
+
43
+ async trackActiveWin() {
44
+ if (this.failedAttempts === 5) {
45
+ logger.warn("active-win.js failed 5 attempts, stopping interval", {
46
+ recorderId: this.recorderId,
47
+ screenId: this.screenId,
48
+ });
49
+ this.stop();
50
+ return;
51
+ }
52
+
53
+ try {
54
+ let awin = null;
55
+
56
+ // Try native module
57
+ if (activeWindowSync) {
58
+ awin = activeWindowSync();
59
+ } else {
60
+ // No tracking method available
61
+ if (this.failedAttempts === 0) {
62
+ logger.debug("Active window tracking unavailable (get-windows not available)");
63
+ }
64
+ this.failedAttempts = 5;
65
+ this.stop();
66
+ return;
67
+ }
68
+
69
+ if (awin) {
70
+ const result = {
71
+ title: awin.title,
72
+ time: Date.now(),
73
+ owner: {
74
+ id: awin.owner?.bundleId || awin.owner?.path,
75
+ name: normalizeAppName(awin.owner?.name),
76
+ },
77
+ };
78
+
79
+ // Extract icon for this application
80
+ await extractIcon({ name: result.owner.name, id: result.owner.id });
81
+
82
+ // Write to JSONL file
83
+ jsonl.append(path.join(this.directory, this.fileName), result);
84
+
85
+ logger.silly("Tracked active window", {
86
+ app: result.owner.name,
87
+ title: result.title?.substring(0, 50)
88
+ });
89
+ } else {
90
+ logger.debug("active-win.js activeWindowSync() returned nullable value");
91
+ this.failedAttempts++;
92
+ }
93
+ } catch (err) {
94
+ logger.warn("active-win.js error", {
95
+ error: err.message,
96
+ attempt: this.failedAttempts + 1
97
+ });
98
+ this.failedAttempts++;
99
+ }
100
+ }
101
+
102
+ start() {
103
+ const logExit = logFunctionCall('ActiveWin.start');
104
+
105
+ logger.debug("active-win.js starting tracking active win", {
106
+ recorderId: this.recorderId,
107
+ screenId: this.screenId,
108
+ directory: this.directory,
109
+ fileName: this.fileName,
110
+ });
111
+
112
+ if (!this.screenId || !this.directory || !this.fileName) {
113
+ logger.warn("active-win.js missing args", {
114
+ recorderId: this.recorderId,
115
+ screenId: this.screenId,
116
+ });
117
+ logExit();
118
+ return;
119
+ }
120
+
121
+ if (!this.intervalRef) {
122
+ logger.debug("active-win.js active win start interval");
123
+ this.intervalRef = setInterval(() => this.trackActiveWin(), 10000);
124
+ } else {
125
+ logger.debug("active-win.js active win already started", {
126
+ recorderId: this.recorderId,
127
+ screenId: this.screenId,
128
+ });
129
+ }
130
+
131
+ logExit();
132
+ }
133
+
134
+ stop = () => {
135
+ const logExit = logFunctionCall('ActiveWin.stop');
136
+
137
+ logger.debug("active-win.js removing tracker for screen", {
138
+ recorderId: this.recorderId,
139
+ screenId: this.screenId,
140
+ });
141
+
142
+ if (this.intervalRef) {
143
+ clearInterval(this.intervalRef);
144
+ }
145
+
146
+ this.intervalRef = null;
147
+ logExit();
148
+ };
149
+ }
150
+
151
+ class ActiveWinManager {
152
+ constructor() {
153
+ this.activeWins = [];
154
+ }
155
+
156
+ startNew({ recorderId, screenId, directory, fileName }) {
157
+ const logExit = logFunctionCall('ActiveWinManager.startNew');
158
+
159
+ const activeWin = new ActiveWin({
160
+ recorderId,
161
+ screenId,
162
+ directory,
163
+ fileName,
164
+ });
165
+
166
+ activeWin.start();
167
+ this.activeWins.push(activeWin);
168
+
169
+ logger.debug("Started new active window tracker", {
170
+ recorderId,
171
+ screenId,
172
+ totalTrackers: this.activeWins.length
173
+ });
174
+
175
+ logExit();
176
+ }
177
+
178
+ stop({ recorderId, screenId }) {
179
+ const logExit = logFunctionCall('ActiveWinManager.stop');
180
+
181
+ const index = this.activeWins.findIndex(
182
+ (win) => win.recorderId === recorderId && win.screenId === screenId
183
+ );
184
+
185
+ if (index > -1) {
186
+ const activeWin = this.activeWins[index];
187
+ activeWin.stop();
188
+ this.activeWins.splice(index, 1);
189
+
190
+ logger.debug("Stopped active window tracker", {
191
+ recorderId,
192
+ screenId,
193
+ remainingTrackers: this.activeWins.length
194
+ });
195
+ }
196
+
197
+ logExit();
198
+ }
199
+
200
+ stopAll() {
201
+ const logExit = logFunctionCall('ActiveWinManager.stopAll');
202
+
203
+ logger.info("active-win.js stopping all active wins");
204
+ this.activeWins.forEach((activeWin) => activeWin.stop());
205
+ this.activeWins = [];
206
+
207
+ logExit();
208
+ }
209
+ }
210
+
211
+ const activeWinManager = new ActiveWinManager();
212
+ module.exports = activeWinManager;
@@ -0,0 +1,39 @@
1
+ import { logger } from "../../logger.js";
2
+
3
+ const getIconAsBuffer = async (bundleId) => {
4
+ try {
5
+ // Try to import the file-icon package for macOS icon extraction
6
+ // This may fail in pkg builds where native modules don't work
7
+ const { fileIconToBuffer } = await import("file-icon");
8
+
9
+ logger.debug("Extracting icon for macOS app", { bundleId });
10
+
11
+ const buffer = await fileIconToBuffer(bundleId);
12
+ if (!buffer) {
13
+ logger.debug("No icon buffer returned for bundle", { bundleId });
14
+ return null;
15
+ }
16
+
17
+ logger.debug("Successfully extracted macOS icon", {
18
+ bundleId,
19
+ bufferSize: buffer.length
20
+ });
21
+
22
+ return { extension: "png", buffer };
23
+ } catch (error) {
24
+ // Don't log warnings for module loading errors in pkg builds
25
+ if (error.code === 'MODULE_NOT_FOUND' || error.message.includes('Dynamic require')) {
26
+ logger.debug("Icon extraction unavailable (native module not loaded)", {
27
+ isPkg: typeof process.pkg !== 'undefined'
28
+ });
29
+ } else {
30
+ logger.warn("Failed to extract macOS icon", {
31
+ bundleId,
32
+ error: error.message
33
+ });
34
+ }
35
+ return null;
36
+ }
37
+ };
38
+
39
+ export { getIconAsBuffer };