@myerscarpenter/quest-dev 1.0.8 → 1.2.0
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/build/commands/battery.d.ts +9 -0
- package/build/commands/battery.d.ts.map +1 -0
- package/build/commands/battery.js +31 -0
- package/build/commands/battery.js.map +1 -0
- package/build/commands/logcat.d.ts +24 -0
- package/build/commands/logcat.d.ts.map +1 -0
- package/build/commands/logcat.js +261 -0
- package/build/commands/logcat.js.map +1 -0
- package/build/commands/open.d.ts +1 -1
- package/build/commands/open.d.ts.map +1 -1
- package/build/commands/open.js +14 -14
- package/build/commands/open.js.map +1 -1
- package/build/commands/screenshot.d.ts +1 -1
- package/build/commands/screenshot.d.ts.map +1 -1
- package/build/commands/screenshot.js +55 -9
- package/build/commands/screenshot.js.map +1 -1
- package/build/commands/stay-awake.d.ts +14 -0
- package/build/commands/stay-awake.d.ts.map +1 -0
- package/build/commands/stay-awake.js +234 -0
- package/build/commands/stay-awake.js.map +1 -0
- package/build/index.js +84 -5
- package/build/index.js.map +1 -1
- package/build/utils/adb.d.ts +12 -7
- package/build/utils/adb.d.ts.map +1 -1
- package/build/utils/adb.js +138 -31
- package/build/utils/adb.js.map +1 -1
- package/build/utils/filename.d.ts +9 -0
- package/build/utils/filename.d.ts.map +1 -0
- package/build/utils/filename.js +17 -0
- package/build/utils/filename.js.map +1 -0
- package/build/utils/filename.test.d.ts +5 -0
- package/build/utils/filename.test.d.ts.map +1 -0
- package/build/utils/filename.test.js +40 -0
- package/build/utils/filename.test.js.map +1 -0
- package/package.json +2 -1
- package/src/commands/battery.ts +34 -0
- package/src/commands/logcat.ts +288 -0
- package/src/commands/open.ts +18 -14
- package/src/commands/screenshot.ts +61 -9
- package/src/commands/stay-awake.ts +254 -0
- package/src/index.ts +119 -9
- package/src/utils/adb.ts +148 -31
- package/src/utils/filename.test.ts +55 -0
- package/src/utils/filename.ts +18 -0
- package/tests/adb.test.ts +2 -2
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quest stay-awake command
|
|
3
|
+
* Keeps Quest screen awake by setting screen timeout to 24 hours
|
|
4
|
+
* Restores original timeout on exit (Ctrl-C)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { checkADBPath } from '../utils/adb.js';
|
|
8
|
+
import { execCommand } from '../utils/exec.js';
|
|
9
|
+
import { execSync, spawn, ChildProcess } from 'child_process';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get current screen timeout setting
|
|
15
|
+
*/
|
|
16
|
+
async function getScreenTimeout(): Promise<number> {
|
|
17
|
+
const output = await execCommand('adb', ['shell', 'settings', 'get', 'system', 'screen_off_timeout']);
|
|
18
|
+
return parseInt(output.trim(), 10);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Set screen timeout (in milliseconds)
|
|
23
|
+
*/
|
|
24
|
+
async function setScreenTimeout(timeout: number): Promise<void> {
|
|
25
|
+
await execCommand('adb', ['shell', 'settings', 'put', 'system', 'screen_off_timeout', timeout.toString()]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Disable Quest proximity sensor (keeps screen on even when not worn)
|
|
30
|
+
*/
|
|
31
|
+
async function disableProximitySensor(): Promise<void> {
|
|
32
|
+
await execCommand('adb', ['shell', 'am', 'broadcast', '-a', 'com.oculus.vrpowermanager.prox_close']);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Enable Quest proximity sensor (re-enable normal behavior)
|
|
37
|
+
* Note: automation_disable actually RE-ENABLES normal proximity sensor automation
|
|
38
|
+
*/
|
|
39
|
+
async function enableProximitySensor(): Promise<void> {
|
|
40
|
+
await execCommand('adb', ['shell', 'am', 'broadcast', '-a', 'com.oculus.vrpowermanager.automation_disable']);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wake the Quest screen
|
|
45
|
+
*/
|
|
46
|
+
async function wakeScreen(): Promise<void> {
|
|
47
|
+
await execCommand('adb', ['shell', 'input', 'keyevent', 'KEYCODE_WAKEUP']);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Child watchdog process - polls for parent death and cleans up
|
|
52
|
+
*/
|
|
53
|
+
export async function stayAwakeWatchdog(parentPid: number, originalTimeout: number): Promise<void> {
|
|
54
|
+
const pollInterval = 5000; // Check every 5 seconds
|
|
55
|
+
|
|
56
|
+
const checkParent = setInterval(() => {
|
|
57
|
+
try {
|
|
58
|
+
// Check if parent process still exists
|
|
59
|
+
process.kill(parentPid, 0);
|
|
60
|
+
// Parent still alive, continue polling
|
|
61
|
+
} catch {
|
|
62
|
+
// Parent is dead - perform cleanup
|
|
63
|
+
console.log('Parent process died, restoring Quest settings...');
|
|
64
|
+
clearInterval(checkParent);
|
|
65
|
+
|
|
66
|
+
// Restore settings synchronously
|
|
67
|
+
try {
|
|
68
|
+
execSync(`adb shell settings put system screen_off_timeout ${originalTimeout}`, { stdio: 'ignore' });
|
|
69
|
+
execSync(`adb shell am broadcast -a com.oculus.vrpowermanager.automation_disable`, { stdio: 'ignore' });
|
|
70
|
+
|
|
71
|
+
// Cleanup PID file
|
|
72
|
+
const pidFile = `${os.homedir()}/.quest-dev-stay-awake.pid`;
|
|
73
|
+
try {
|
|
74
|
+
fs.unlinkSync(pidFile);
|
|
75
|
+
} catch {}
|
|
76
|
+
|
|
77
|
+
console.log(`Screen timeout restored to ${originalTimeout}ms (${Math.round(originalTimeout / 1000)}s)`);
|
|
78
|
+
console.log('Proximity sensor re-enabled');
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error('Failed to restore settings:', (err as Error).message);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
}, pollInterval);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Main stay-awake command handler
|
|
90
|
+
*/
|
|
91
|
+
export async function stayAwakeCommand(idleTimeout: number = 300000): Promise<void> {
|
|
92
|
+
// Check prerequisites
|
|
93
|
+
checkADBPath();
|
|
94
|
+
|
|
95
|
+
// Check devices without verbose output
|
|
96
|
+
try {
|
|
97
|
+
const output = await execCommand('adb', ['devices']);
|
|
98
|
+
const lines = output.trim().split('\n').slice(1);
|
|
99
|
+
const devices = lines.filter(line => line.trim() && !line.includes('List of devices'));
|
|
100
|
+
|
|
101
|
+
if (devices.length === 0) {
|
|
102
|
+
console.error('Error: No ADB devices connected');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Error: Failed to list ADB devices');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// PID file management
|
|
111
|
+
const pidFilePath = `${os.homedir()}/.quest-dev-stay-awake.pid`;
|
|
112
|
+
|
|
113
|
+
// Check for existing process
|
|
114
|
+
if (fs.existsSync(pidFilePath)) {
|
|
115
|
+
const existingPid = parseInt(fs.readFileSync(pidFilePath, 'utf-8'));
|
|
116
|
+
try {
|
|
117
|
+
process.kill(existingPid, 0); // Test if process exists
|
|
118
|
+
console.error(`Error: stay-awake is already running (PID: ${existingPid})`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
} catch {
|
|
121
|
+
// Process dead, cleanup stale PID file
|
|
122
|
+
fs.unlinkSync(pidFilePath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Get original timeout
|
|
127
|
+
let originalTimeout: number;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
originalTimeout = await getScreenTimeout();
|
|
131
|
+
console.log(`Original screen timeout: ${originalTimeout}ms (${Math.round(originalTimeout / 1000)}s)`);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Failed to get current screen timeout');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Write PID file
|
|
138
|
+
try {
|
|
139
|
+
fs.writeFileSync(pidFilePath, process.pid.toString());
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.warn('Failed to write PID file, hook will not work');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Spawn child watchdog process
|
|
145
|
+
let childProcess: ChildProcess | null = null;
|
|
146
|
+
try {
|
|
147
|
+
childProcess = spawn(process.execPath, [
|
|
148
|
+
process.argv[1], // quest-dev script path
|
|
149
|
+
'stay-awake-watchdog',
|
|
150
|
+
'--parent-pid', process.pid.toString(),
|
|
151
|
+
'--original-timeout', originalTimeout.toString()
|
|
152
|
+
], {
|
|
153
|
+
detached: true,
|
|
154
|
+
stdio: 'ignore'
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
childProcess.unref(); // Allow parent to exit without waiting for child
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.warn('Failed to spawn watchdog child process');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Wake screen and disable proximity sensor
|
|
163
|
+
try {
|
|
164
|
+
await wakeScreen();
|
|
165
|
+
console.log('Quest screen woken up');
|
|
166
|
+
|
|
167
|
+
await disableProximitySensor();
|
|
168
|
+
console.log('Proximity sensor disabled');
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('Failed to wake screen or disable proximity sensor:', (error as Error).message);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Set timeout to 24 hours (86400000ms)
|
|
174
|
+
const longTimeout = 86400000;
|
|
175
|
+
try {
|
|
176
|
+
await setScreenTimeout(longTimeout);
|
|
177
|
+
console.log(`Screen timeout set to 24 hours`);
|
|
178
|
+
console.log(`Quest will stay awake (idle timeout: ${Math.round(idleTimeout / 1000)}s). Press Ctrl-C to restore original settings.`);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error('Failed to set screen timeout');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Idle timer mechanism
|
|
185
|
+
let idleTimerHandle: NodeJS.Timeout | null = null;
|
|
186
|
+
let cleanupInProgress = false;
|
|
187
|
+
|
|
188
|
+
const resetIdleTimer = () => {
|
|
189
|
+
if (idleTimerHandle) clearTimeout(idleTimerHandle);
|
|
190
|
+
idleTimerHandle = setTimeout(() => {
|
|
191
|
+
console.log('\nIdle timeout reached, exiting...');
|
|
192
|
+
cleanup();
|
|
193
|
+
}, idleTimeout);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Set up cleanup on exit (must be synchronous for signal handlers)
|
|
197
|
+
const cleanup = () => {
|
|
198
|
+
if (cleanupInProgress) return; // Guard against double-cleanup
|
|
199
|
+
cleanupInProgress = true;
|
|
200
|
+
|
|
201
|
+
// Clear idle timer
|
|
202
|
+
if (idleTimerHandle) clearTimeout(idleTimerHandle);
|
|
203
|
+
|
|
204
|
+
// Kill child watchdog
|
|
205
|
+
if (childProcess) {
|
|
206
|
+
try {
|
|
207
|
+
childProcess.kill();
|
|
208
|
+
} catch {}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log('\nRestoring original settings...');
|
|
212
|
+
try {
|
|
213
|
+
// Remove PID file
|
|
214
|
+
try {
|
|
215
|
+
fs.unlinkSync(pidFilePath);
|
|
216
|
+
} catch {}
|
|
217
|
+
|
|
218
|
+
// Restore Quest settings
|
|
219
|
+
execSync(`adb shell settings put system screen_off_timeout ${originalTimeout}`, { stdio: 'ignore' });
|
|
220
|
+
execSync(`adb shell am broadcast -a com.oculus.vrpowermanager.automation_disable`, { stdio: 'ignore' });
|
|
221
|
+
console.log(`Screen timeout restored to ${originalTimeout}ms (${Math.round(originalTimeout / 1000)}s)`);
|
|
222
|
+
console.log(`Proximity sensor re-enabled`);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('Failed to restore settings:', (error as Error).message);
|
|
225
|
+
}
|
|
226
|
+
process.exit(0);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Handle Ctrl-C and termination
|
|
230
|
+
process.on('SIGINT', cleanup);
|
|
231
|
+
process.on('SIGTERM', cleanup);
|
|
232
|
+
process.on('SIGHUP', cleanup);
|
|
233
|
+
|
|
234
|
+
// Handle SIGUSR1 for activity reset
|
|
235
|
+
process.on('SIGUSR1', () => {
|
|
236
|
+
console.log('Activity detected, resetting idle timer');
|
|
237
|
+
resetIdleTimer();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Start idle timer
|
|
241
|
+
resetIdleTimer();
|
|
242
|
+
|
|
243
|
+
// Keep process running with an interval that does nothing
|
|
244
|
+
console.log('Keeping Quest awake...');
|
|
245
|
+
setInterval(() => {
|
|
246
|
+
// Do nothing, just keep process alive
|
|
247
|
+
}, 60000); // Check every minute
|
|
248
|
+
|
|
249
|
+
// Prevent process from exiting
|
|
250
|
+
await new Promise<void>((resolve) => {
|
|
251
|
+
// This will only resolve when cleanup is called
|
|
252
|
+
process.on('exit', () => resolve());
|
|
253
|
+
});
|
|
254
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,9 @@ import { fileURLToPath } from 'url';
|
|
|
12
12
|
import { dirname, join } from 'path';
|
|
13
13
|
import { screenshotCommand } from './commands/screenshot.js';
|
|
14
14
|
import { openCommand } from './commands/open.js';
|
|
15
|
+
import { startCommand, stopCommand, statusCommand, tailCommand } from './commands/logcat.js';
|
|
16
|
+
import { batteryCommand } from './commands/battery.js';
|
|
17
|
+
import { stayAwakeCommand, stayAwakeWatchdog } from './commands/stay-awake.js';
|
|
15
18
|
|
|
16
19
|
// Read version from package.json
|
|
17
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -43,17 +46,26 @@ const cli = yargs(hideBin(process.argv))
|
|
|
43
46
|
|
|
44
47
|
// Screenshot command
|
|
45
48
|
cli.command(
|
|
46
|
-
'screenshot <
|
|
47
|
-
'Take a screenshot from Quest and save to
|
|
49
|
+
'screenshot <directory>',
|
|
50
|
+
'Take a screenshot from Quest and save to directory with auto-generated filename',
|
|
48
51
|
(yargs) => {
|
|
49
|
-
return yargs
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
return yargs
|
|
53
|
+
.positional('directory', {
|
|
54
|
+
describe: 'Output directory path (e.g., ~/screenshots)',
|
|
55
|
+
type: 'string',
|
|
56
|
+
demandOption: true
|
|
57
|
+
})
|
|
58
|
+
.option('caption', {
|
|
59
|
+
describe: 'Caption to embed in JPEG COM metadata',
|
|
60
|
+
type: 'string',
|
|
61
|
+
alias: 'c'
|
|
62
|
+
});
|
|
54
63
|
},
|
|
55
64
|
async (argv) => {
|
|
56
|
-
await screenshotCommand(
|
|
65
|
+
await screenshotCommand(
|
|
66
|
+
argv.directory as string,
|
|
67
|
+
argv.caption as string | undefined
|
|
68
|
+
);
|
|
57
69
|
}
|
|
58
70
|
);
|
|
59
71
|
|
|
@@ -72,10 +84,108 @@ cli.command(
|
|
|
72
84
|
describe: 'Close all other tabs before opening',
|
|
73
85
|
type: 'boolean',
|
|
74
86
|
default: false
|
|
87
|
+
})
|
|
88
|
+
.option('browser', {
|
|
89
|
+
describe: 'Browser package name (e.g., com.oculus.browser, org.chromium.chrome)',
|
|
90
|
+
type: 'string',
|
|
91
|
+
default: 'com.oculus.browser',
|
|
92
|
+
alias: 'b'
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
async (argv) => {
|
|
96
|
+
await openCommand(
|
|
97
|
+
argv.url as string,
|
|
98
|
+
argv.closeOthers as boolean,
|
|
99
|
+
argv.browser as string
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Logcat command
|
|
105
|
+
cli.command(
|
|
106
|
+
'logcat <action>',
|
|
107
|
+
'Capture Android logcat to files (CRITICAL: always start before testing to avoid losing crash logs)',
|
|
108
|
+
(yargs) => {
|
|
109
|
+
return yargs
|
|
110
|
+
.positional('action', {
|
|
111
|
+
describe: 'Action to perform',
|
|
112
|
+
type: 'string',
|
|
113
|
+
choices: ['start', 'stop', 'status', 'tail'],
|
|
114
|
+
demandOption: true
|
|
115
|
+
})
|
|
116
|
+
.option('filter', {
|
|
117
|
+
describe: 'Logcat filter expression (e.g., "*:W" for warnings+, "chromium:V *:S" for chromium only)',
|
|
118
|
+
type: 'string'
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
async (argv) => {
|
|
122
|
+
const action = argv.action as string;
|
|
123
|
+
const filter = argv.filter as string | undefined;
|
|
124
|
+
|
|
125
|
+
switch (action) {
|
|
126
|
+
case 'start':
|
|
127
|
+
await startCommand(filter);
|
|
128
|
+
break;
|
|
129
|
+
case 'stop':
|
|
130
|
+
await stopCommand();
|
|
131
|
+
break;
|
|
132
|
+
case 'status':
|
|
133
|
+
await statusCommand();
|
|
134
|
+
break;
|
|
135
|
+
case 'tail':
|
|
136
|
+
await tailCommand();
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
console.error(`Unknown action: ${action}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Battery command
|
|
146
|
+
cli.command(
|
|
147
|
+
'battery',
|
|
148
|
+
'Show Quest battery percentage and charging status',
|
|
149
|
+
() => {},
|
|
150
|
+
async () => {
|
|
151
|
+
await batteryCommand();
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Stay-awake command
|
|
156
|
+
cli.command(
|
|
157
|
+
'stay-awake',
|
|
158
|
+
'Keep Quest screen awake (sets 24hr timeout, restores on Ctrl-C)',
|
|
159
|
+
(yargs) => {
|
|
160
|
+
return yargs.option('idle-timeout', {
|
|
161
|
+
describe: 'Idle timeout in milliseconds (default: 300000 = 5 minutes)',
|
|
162
|
+
type: 'number',
|
|
163
|
+
default: 300000,
|
|
164
|
+
alias: 'i'
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
async (argv) => {
|
|
168
|
+
await stayAwakeCommand(argv.idleTimeout as number);
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Stay-awake watchdog (internal subcommand, spawned by stay-awake parent)
|
|
173
|
+
cli.command(
|
|
174
|
+
'stay-awake-watchdog',
|
|
175
|
+
false as any, // Hide from help
|
|
176
|
+
(yargs) => {
|
|
177
|
+
return yargs
|
|
178
|
+
.option('parent-pid', {
|
|
179
|
+
type: 'number',
|
|
180
|
+
demandOption: true
|
|
181
|
+
})
|
|
182
|
+
.option('original-timeout', {
|
|
183
|
+
type: 'number',
|
|
184
|
+
demandOption: true
|
|
75
185
|
});
|
|
76
186
|
},
|
|
77
187
|
async (argv) => {
|
|
78
|
-
await
|
|
188
|
+
await stayAwakeWatchdog(argv.parentPid as number, argv.originalTimeout as number);
|
|
79
189
|
}
|
|
80
190
|
);
|
|
81
191
|
|
package/src/utils/adb.ts
CHANGED
|
@@ -8,13 +8,69 @@ import { execCommand, execCommandFull } from './exec.js';
|
|
|
8
8
|
|
|
9
9
|
const CDP_PORT = 9223; // Chrome DevTools Protocol port (Quest browser default)
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Get browser process PID
|
|
13
|
+
*/
|
|
14
|
+
async function getBrowserPID(packageName: string): Promise<number | null> {
|
|
15
|
+
try {
|
|
16
|
+
const result = await execCommandFull('adb', ['shell', `ps | grep ${packageName}`]);
|
|
17
|
+
if (!result.stdout) return null;
|
|
18
|
+
|
|
19
|
+
// Parse ps output: USER PID PPID ... NAME
|
|
20
|
+
const lines = result.stdout.trim().split('\n');
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
if (line.includes('grep')) continue; // Skip grep itself
|
|
23
|
+
const parts = line.trim().split(/\s+/);
|
|
24
|
+
if (parts.length >= 2) {
|
|
25
|
+
return parseInt(parts[1], 10); // PID is second column
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect CDP socket for a browser
|
|
36
|
+
* Returns socket name (e.g., "chrome_devtools_remote_12345")
|
|
37
|
+
*/
|
|
38
|
+
async function detectCDPSocket(packageName: string): Promise<string> {
|
|
39
|
+
const pid = await getBrowserPID(packageName);
|
|
40
|
+
|
|
41
|
+
if (pid) {
|
|
42
|
+
// Try PID-based socket first
|
|
43
|
+
try {
|
|
44
|
+
const result = await execCommandFull('adb', [
|
|
45
|
+
'shell',
|
|
46
|
+
`cat /proc/net/unix | grep chrome_devtools_remote_${pid}`
|
|
47
|
+
]);
|
|
48
|
+
if (result.stdout.includes(`chrome_devtools_remote_${pid}`)) {
|
|
49
|
+
return `chrome_devtools_remote_${pid}`;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Fall through to default
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Default: generic socket (Quest Browser)
|
|
57
|
+
return 'chrome_devtools_remote';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get CDP port for a socket
|
|
62
|
+
* Generic socket uses 9223, PID-based uses 9222
|
|
63
|
+
*/
|
|
64
|
+
function getCDPPortForSocket(socket: string): number {
|
|
65
|
+
return socket === 'chrome_devtools_remote' ? 9223 : 9222;
|
|
66
|
+
}
|
|
67
|
+
|
|
11
68
|
/**
|
|
12
69
|
* Check if ADB is available on PATH
|
|
13
70
|
*/
|
|
14
71
|
export function checkADBPath(): string {
|
|
15
72
|
try {
|
|
16
73
|
const adbPath = which.sync('adb');
|
|
17
|
-
console.log(`Found ADB at: ${adbPath}`);
|
|
18
74
|
return adbPath;
|
|
19
75
|
} catch (error) {
|
|
20
76
|
console.error('Error: ADB not found in PATH');
|
|
@@ -116,8 +172,15 @@ export function isPortListening(port: number): Promise<boolean> {
|
|
|
116
172
|
/**
|
|
117
173
|
* Idempotently set up ADB port forwarding for a given port
|
|
118
174
|
*/
|
|
119
|
-
export async function ensurePortForwarding(
|
|
175
|
+
export async function ensurePortForwarding(
|
|
176
|
+
port: number,
|
|
177
|
+
browser: string = 'com.oculus.browser'
|
|
178
|
+
): Promise<void> {
|
|
120
179
|
try {
|
|
180
|
+
// Detect CDP socket and port for this browser
|
|
181
|
+
const cdpSocket = await detectCDPSocket(browser);
|
|
182
|
+
const cdpPort = getCDPPortForSocket(cdpSocket);
|
|
183
|
+
|
|
121
184
|
// Check reverse forwarding (Quest -> Host for dev server)
|
|
122
185
|
const reverseList = await execCommand('adb', ['reverse', '--list']);
|
|
123
186
|
const reverseExists = reverseList.includes(`tcp:${port}`);
|
|
@@ -132,27 +195,27 @@ export async function ensurePortForwarding(port: number): Promise<void> {
|
|
|
132
195
|
// Check forward forwarding (Host -> Quest for CDP)
|
|
133
196
|
// First check if ADB already has this forwarding set up
|
|
134
197
|
const forwardList = await execCommand('adb', ['forward', '--list']);
|
|
135
|
-
const forwardExists = forwardList.includes(`tcp:${
|
|
198
|
+
const forwardExists = forwardList.includes(`tcp:${cdpPort}`) && forwardList.includes(cdpSocket);
|
|
136
199
|
|
|
137
200
|
if (forwardExists) {
|
|
138
|
-
console.log(`CDP port ${
|
|
201
|
+
console.log(`CDP port ${cdpPort} forwarding already set up`);
|
|
139
202
|
} else {
|
|
140
203
|
// Check if something else is using the port
|
|
141
|
-
const cdpPortListening = await isPortListening(
|
|
204
|
+
const cdpPortListening = await isPortListening(cdpPort);
|
|
142
205
|
if (cdpPortListening) {
|
|
143
|
-
console.error(`Error: Port ${
|
|
206
|
+
console.error(`Error: Port ${cdpPort} is already in use by another process`);
|
|
144
207
|
console.error('');
|
|
145
|
-
console.error(
|
|
208
|
+
console.error(`CDP port forwarding requires port ${cdpPort} to be free.`);
|
|
146
209
|
console.error('Please stop the process using this port and try again.');
|
|
147
210
|
console.error('');
|
|
148
211
|
console.error('To find what is using the port:');
|
|
149
|
-
console.error(` lsof -i :${
|
|
212
|
+
console.error(` lsof -i :${cdpPort}`);
|
|
150
213
|
console.error('');
|
|
151
214
|
process.exit(1);
|
|
152
215
|
}
|
|
153
216
|
|
|
154
|
-
await execCommand('adb', ['forward', `tcp:${
|
|
155
|
-
console.log(`ADB forward port forwarding set up: Host:${
|
|
217
|
+
await execCommand('adb', ['forward', `tcp:${cdpPort}`, `localabstract:${cdpSocket}`]);
|
|
218
|
+
console.log(`ADB forward port forwarding set up: Host:${cdpPort} -> Quest:${cdpSocket} (CDP)`);
|
|
156
219
|
}
|
|
157
220
|
} catch (error) {
|
|
158
221
|
console.error('Failed to set up port forwarding:', (error as Error).message);
|
|
@@ -161,22 +224,22 @@ export async function ensurePortForwarding(port: number): Promise<void> {
|
|
|
161
224
|
}
|
|
162
225
|
|
|
163
226
|
/**
|
|
164
|
-
* Check if
|
|
227
|
+
* Check if browser is running
|
|
165
228
|
*/
|
|
166
|
-
export async function isBrowserRunning(): Promise<boolean> {
|
|
229
|
+
export async function isBrowserRunning(browser: string = 'com.oculus.browser'): Promise<boolean> {
|
|
167
230
|
try {
|
|
168
|
-
const result = await execCommandFull('adb', ['shell',
|
|
169
|
-
return result.stdout.includes(
|
|
231
|
+
const result = await execCommandFull('adb', ['shell', `ps | grep ${browser}`]);
|
|
232
|
+
return result.stdout.includes(browser);
|
|
170
233
|
} catch (error) {
|
|
171
234
|
return false;
|
|
172
235
|
}
|
|
173
236
|
}
|
|
174
237
|
|
|
175
238
|
/**
|
|
176
|
-
* Launch
|
|
239
|
+
* Launch browser with a URL using am start
|
|
177
240
|
*/
|
|
178
|
-
export async function launchBrowser(url: string): Promise<boolean> {
|
|
179
|
-
console.log('Launching
|
|
241
|
+
export async function launchBrowser(url: string, browser: string = 'com.oculus.browser'): Promise<boolean> {
|
|
242
|
+
console.log('Launching browser...');
|
|
180
243
|
try {
|
|
181
244
|
await execCommand('adb', [
|
|
182
245
|
'shell',
|
|
@@ -186,12 +249,12 @@ export async function launchBrowser(url: string): Promise<boolean> {
|
|
|
186
249
|
'android.intent.action.VIEW',
|
|
187
250
|
'-d',
|
|
188
251
|
url,
|
|
189
|
-
|
|
252
|
+
browser
|
|
190
253
|
]);
|
|
191
|
-
console.log(`
|
|
254
|
+
console.log(`Browser launched with URL: ${url}`);
|
|
192
255
|
return true;
|
|
193
256
|
} catch (error) {
|
|
194
|
-
console.error('Failed to launch
|
|
257
|
+
console.error('Failed to launch browser:', (error as Error).message);
|
|
195
258
|
return false;
|
|
196
259
|
}
|
|
197
260
|
}
|
|
@@ -199,38 +262,45 @@ export async function launchBrowser(url: string): Promise<boolean> {
|
|
|
199
262
|
/**
|
|
200
263
|
* Get CDP port
|
|
201
264
|
*/
|
|
202
|
-
export function getCDPPort(): number {
|
|
203
|
-
|
|
265
|
+
export async function getCDPPort(browser: string = 'com.oculus.browser'): Promise<number> {
|
|
266
|
+
const cdpSocket = await detectCDPSocket(browser);
|
|
267
|
+
return getCDPPortForSocket(cdpSocket);
|
|
204
268
|
}
|
|
205
269
|
|
|
206
270
|
/**
|
|
207
271
|
* Set up only CDP forwarding (for external URLs that don't need reverse forwarding)
|
|
208
272
|
*/
|
|
209
|
-
export async function ensureCDPForwarding(
|
|
273
|
+
export async function ensureCDPForwarding(
|
|
274
|
+
browser: string = 'com.oculus.browser'
|
|
275
|
+
): Promise<void> {
|
|
210
276
|
try {
|
|
277
|
+
// Detect CDP socket and port for this browser
|
|
278
|
+
const cdpSocket = await detectCDPSocket(browser);
|
|
279
|
+
const cdpPort = getCDPPortForSocket(cdpSocket);
|
|
280
|
+
|
|
211
281
|
// Check forward forwarding (Host -> Quest for CDP)
|
|
212
282
|
const forwardList = await execCommand('adb', ['forward', '--list']);
|
|
213
|
-
const forwardExists = forwardList.includes(`tcp:${
|
|
283
|
+
const forwardExists = forwardList.includes(`tcp:${cdpPort}`) && forwardList.includes(cdpSocket);
|
|
214
284
|
|
|
215
285
|
if (forwardExists) {
|
|
216
|
-
console.log(`CDP port ${
|
|
286
|
+
console.log(`CDP port ${cdpPort} forwarding already set up`);
|
|
217
287
|
} else {
|
|
218
288
|
// Check if something else is using the port
|
|
219
|
-
const cdpPortListening = await isPortListening(
|
|
289
|
+
const cdpPortListening = await isPortListening(cdpPort);
|
|
220
290
|
if (cdpPortListening) {
|
|
221
|
-
console.error(`Error: Port ${
|
|
291
|
+
console.error(`Error: Port ${cdpPort} is already in use by another process`);
|
|
222
292
|
console.error('');
|
|
223
|
-
console.error(
|
|
293
|
+
console.error(`CDP port forwarding requires port ${cdpPort} to be free.`);
|
|
224
294
|
console.error('Please stop the process using this port and try again.');
|
|
225
295
|
console.error('');
|
|
226
296
|
console.error('To find what is using the port:');
|
|
227
|
-
console.error(` lsof -i :${
|
|
297
|
+
console.error(` lsof -i :${cdpPort}`);
|
|
228
298
|
console.error('');
|
|
229
299
|
process.exit(1);
|
|
230
300
|
}
|
|
231
301
|
|
|
232
|
-
await execCommand('adb', ['forward', `tcp:${
|
|
233
|
-
console.log(`ADB forward port forwarding set up: Host:${
|
|
302
|
+
await execCommand('adb', ['forward', `tcp:${cdpPort}`, `localabstract:${cdpSocket}`]);
|
|
303
|
+
console.log(`ADB forward port forwarding set up: Host:${cdpPort} -> Quest:${cdpSocket} (CDP)`);
|
|
234
304
|
}
|
|
235
305
|
} catch (error) {
|
|
236
306
|
console.error('Failed to set up CDP forwarding:', (error as Error).message);
|
|
@@ -274,3 +344,50 @@ export async function checkQuestAwake(): Promise<void> {
|
|
|
274
344
|
process.exit(1);
|
|
275
345
|
}
|
|
276
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get Quest battery status
|
|
350
|
+
* Returns percentage and charging state in one line
|
|
351
|
+
*/
|
|
352
|
+
export async function getBatteryStatus(): Promise<string> {
|
|
353
|
+
const result = await execCommandFull('adb', ['shell', 'dumpsys', 'battery']);
|
|
354
|
+
|
|
355
|
+
if (result.code !== 0) {
|
|
356
|
+
throw new Error('Failed to get battery status');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Parse battery info
|
|
360
|
+
let level = 0;
|
|
361
|
+
let acPowered = false;
|
|
362
|
+
let usbPowered = false;
|
|
363
|
+
let maxChargingCurrent = 0;
|
|
364
|
+
|
|
365
|
+
const lines = result.stdout.split('\n');
|
|
366
|
+
for (const line of lines) {
|
|
367
|
+
const trimmed = line.trim();
|
|
368
|
+
if (trimmed.startsWith('level: ')) {
|
|
369
|
+
level = parseInt(trimmed.substring(7), 10);
|
|
370
|
+
} else if (trimmed.startsWith('AC powered: ')) {
|
|
371
|
+
acPowered = trimmed.substring(12) === 'true';
|
|
372
|
+
} else if (trimmed.startsWith('USB powered: ')) {
|
|
373
|
+
usbPowered = trimmed.substring(13) === 'true';
|
|
374
|
+
} else if (trimmed.startsWith('Max charging current: ')) {
|
|
375
|
+
maxChargingCurrent = parseInt(trimmed.substring(22), 10);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Determine charging state
|
|
380
|
+
let state: string;
|
|
381
|
+
if (acPowered || usbPowered) {
|
|
382
|
+
// Fast charging is typically > 2A (2000000 microamps)
|
|
383
|
+
if (maxChargingCurrent > 2000000) {
|
|
384
|
+
state = 'fast charging';
|
|
385
|
+
} else {
|
|
386
|
+
state = 'charging';
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
state = 'not charging';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return `${level}% ${state}`;
|
|
393
|
+
}
|