neoagent 2.1.4 → 2.1.6
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/lib/manager.js +1 -1
- package/package.json +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +10496 -10399
- package/server/routes/android.js +1 -1
- package/server/services/ai/engine.js +27 -5
- package/server/services/ai/models.js +6 -6
- package/server/services/ai/providers/base.js +1 -0
- package/server/services/ai/providers/ollama.js +21 -0
- package/server/services/android/controller.js +220 -17
package/server/routes/android.js
CHANGED
|
@@ -17,7 +17,7 @@ router.get('/status', async (req, res) => {
|
|
|
17
17
|
router.post('/start', async (req, res) => {
|
|
18
18
|
try {
|
|
19
19
|
const controller = req.app.locals.androidController;
|
|
20
|
-
res.json(await controller.
|
|
20
|
+
res.json(await controller.requestStartEmulator(req.body || {}));
|
|
21
21
|
} catch (err) {
|
|
22
22
|
res.status(500).json({ error: sanitizeError(err) });
|
|
23
23
|
}
|
|
@@ -18,7 +18,7 @@ function generateTitle(task) {
|
|
|
18
18
|
return cleaned.slice(0, 90);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
async function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null) {
|
|
21
|
+
async function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null, providerConfig = {}) {
|
|
22
22
|
const { getSupportedModels, createProviderInstance } = require('./models');
|
|
23
23
|
const models = await getSupportedModels(userId);
|
|
24
24
|
|
|
@@ -68,7 +68,7 @@ async function getProviderForUser(userId, task = '', isSubagent = false, modelOv
|
|
|
68
68
|
if (requested && requested.available !== false && enabledIds.includes(requested.id)) {
|
|
69
69
|
selectedModelDef = requested;
|
|
70
70
|
return {
|
|
71
|
-
provider: createProviderInstance(selectedModelDef.provider, userId),
|
|
71
|
+
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
72
72
|
model: selectedModelDef.id,
|
|
73
73
|
providerName: selectedModelDef.provider
|
|
74
74
|
};
|
|
@@ -102,7 +102,7 @@ async function getProviderForUser(userId, task = '', isSubagent = false, modelOv
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
return {
|
|
105
|
-
provider: createProviderInstance(selectedModelDef.provider, userId),
|
|
105
|
+
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
106
106
|
model: selectedModelDef.id,
|
|
107
107
|
providerName: selectedModelDef.provider
|
|
108
108
|
};
|
|
@@ -271,7 +271,6 @@ class AgentEngine {
|
|
|
271
271
|
const triggerType = options.triggerType || 'user';
|
|
272
272
|
ensureDefaultAiSettings(userId);
|
|
273
273
|
const aiSettings = getAiSettings(userId);
|
|
274
|
-
const { provider, model, providerName } = await getProviderForUser(userId, userMessage, triggerType === 'subagent', _modelOverride);
|
|
275
274
|
|
|
276
275
|
const runId = options.runId || uuidv4();
|
|
277
276
|
const conversationId = options.conversationId;
|
|
@@ -280,6 +279,23 @@ class AgentEngine {
|
|
|
280
279
|
const historyWindow = aiSettings.chat_history_window;
|
|
281
280
|
const toolReplayBudget = aiSettings.tool_replay_budget_chars;
|
|
282
281
|
const maxIterations = this.getIterationLimit(triggerType, aiSettings);
|
|
282
|
+
const providerStatusConfig = {
|
|
283
|
+
onStatus: (status) => {
|
|
284
|
+
if (!status?.message) return;
|
|
285
|
+
this.emit(userId, 'run:interim', {
|
|
286
|
+
runId,
|
|
287
|
+
message: status.message,
|
|
288
|
+
phase: status.phase
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const { provider, model, providerName } = await getProviderForUser(
|
|
293
|
+
userId,
|
|
294
|
+
userMessage,
|
|
295
|
+
triggerType === 'subagent',
|
|
296
|
+
_modelOverride,
|
|
297
|
+
providerStatusConfig
|
|
298
|
+
);
|
|
283
299
|
|
|
284
300
|
const runTitle = generateTitle(userMessage);
|
|
285
301
|
db.prepare(`INSERT OR REPLACE INTO agent_runs(id, user_id, title, status, trigger_type, trigger_source, model)
|
|
@@ -385,7 +401,13 @@ class AgentEngine {
|
|
|
385
401
|
console.error(`[Engine] Model call failed (${model}):`, err.message);
|
|
386
402
|
if (retryForFallback && aiSettings.fallback_model_id && aiSettings.fallback_model_id !== model) {
|
|
387
403
|
console.log(`[Engine] Attempting fallback to: ${aiSettings.fallback_model_id}`);
|
|
388
|
-
const fallback = await getProviderForUser(
|
|
404
|
+
const fallback = await getProviderForUser(
|
|
405
|
+
userId,
|
|
406
|
+
userMessage,
|
|
407
|
+
triggerType === 'subagent',
|
|
408
|
+
aiSettings.fallback_model_id,
|
|
409
|
+
providerStatusConfig
|
|
410
|
+
);
|
|
389
411
|
// Update local state for the retry
|
|
390
412
|
const nextProvider = fallback.provider;
|
|
391
413
|
const nextModel = fallback.model;
|
|
@@ -192,7 +192,7 @@ async function refreshDynamicModels(baseUrl) {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
function createProviderInstance(providerStr, userId = null) {
|
|
195
|
+
function createProviderInstance(providerStr, userId = null, configOverrides = {}) {
|
|
196
196
|
const runtime = getProviderRuntimeConfig(userId, providerStr);
|
|
197
197
|
|
|
198
198
|
if (!runtime.enabled) {
|
|
@@ -203,15 +203,15 @@ function createProviderInstance(providerStr, userId = null) {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
if (providerStr === 'grok') {
|
|
206
|
-
return new GrokProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl });
|
|
206
|
+
return new GrokProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, ...configOverrides });
|
|
207
207
|
} else if (providerStr === 'openai') {
|
|
208
|
-
return new OpenAIProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl });
|
|
208
|
+
return new OpenAIProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, ...configOverrides });
|
|
209
209
|
} else if (providerStr === 'anthropic') {
|
|
210
|
-
return new AnthropicProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl });
|
|
210
|
+
return new AnthropicProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, ...configOverrides });
|
|
211
211
|
} else if (providerStr === 'google') {
|
|
212
|
-
return new GoogleProvider({ apiKey: runtime.apiKey });
|
|
212
|
+
return new GoogleProvider({ apiKey: runtime.apiKey, ...configOverrides });
|
|
213
213
|
} else if (providerStr === 'ollama') {
|
|
214
|
-
return new OllamaProvider({ baseUrl: runtime.baseUrl });
|
|
214
|
+
return new OllamaProvider({ baseUrl: runtime.baseUrl, ...configOverrides });
|
|
215
215
|
}
|
|
216
216
|
throw new Error(`Unknown provider: ${providerStr}`);
|
|
217
217
|
}
|
|
@@ -28,6 +28,13 @@ class OllamaProvider extends BaseProvider {
|
|
|
28
28
|
if (found) return true;
|
|
29
29
|
|
|
30
30
|
console.log(`[Ollama] Model '${model}' not found, pulling from registry...`);
|
|
31
|
+
this.onStatus?.({
|
|
32
|
+
kind: 'model_download',
|
|
33
|
+
status: 'started',
|
|
34
|
+
model,
|
|
35
|
+
phase: 'Downloading model',
|
|
36
|
+
message: `Downloading local Ollama model '${model}'. First-time pulls can take a while.`
|
|
37
|
+
});
|
|
31
38
|
try {
|
|
32
39
|
const res = await fetch(`${this.baseUrl}/api/pull`, {
|
|
33
40
|
method: 'POST',
|
|
@@ -36,10 +43,24 @@ class OllamaProvider extends BaseProvider {
|
|
|
36
43
|
});
|
|
37
44
|
if (!res.ok) throw new Error(`Pull failed: ${res.statusText}`);
|
|
38
45
|
console.log(`[Ollama] Model '${model}' pulled successfully.`);
|
|
46
|
+
this.onStatus?.({
|
|
47
|
+
kind: 'model_download',
|
|
48
|
+
status: 'completed',
|
|
49
|
+
model,
|
|
50
|
+
phase: 'Thinking',
|
|
51
|
+
message: `Local Ollama model '${model}' is ready.`
|
|
52
|
+
});
|
|
39
53
|
// Refresh local model list
|
|
40
54
|
await this.listModels();
|
|
41
55
|
return true;
|
|
42
56
|
} catch (e) {
|
|
57
|
+
this.onStatus?.({
|
|
58
|
+
kind: 'model_download',
|
|
59
|
+
status: 'failed',
|
|
60
|
+
model,
|
|
61
|
+
phase: 'Model download failed',
|
|
62
|
+
message: `Failed to download local Ollama model '${model}': ${e.message}`
|
|
63
|
+
});
|
|
43
64
|
console.error(`[Ollama] Failed to pull model '${model}':`, e.message);
|
|
44
65
|
throw e;
|
|
45
66
|
}
|
|
@@ -47,6 +47,18 @@ function sleep(ms) {
|
|
|
47
47
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function tailFile(filePath, maxLines = 40) {
|
|
51
|
+
try {
|
|
52
|
+
const lines = fs.readFileSync(filePath, 'utf8')
|
|
53
|
+
.split('\n')
|
|
54
|
+
.map((line) => line.trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
return lines.slice(-maxLines);
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
function commandExists(command) {
|
|
51
63
|
const probe = spawnSync('bash', ['-lc', `command -v "${command}"`], { encoding: 'utf8' });
|
|
52
64
|
return probe.status === 0;
|
|
@@ -103,6 +115,10 @@ function configuredSystemImagePlatform() {
|
|
|
103
115
|
return String(process.env.ANDROID_SYSTEM_IMAGE_PLATFORM || '').trim() || null;
|
|
104
116
|
}
|
|
105
117
|
|
|
118
|
+
function shouldForceSdkRefresh() {
|
|
119
|
+
return String(process.env.ANDROID_FORCE_SDK_REFRESH || '').trim().toLowerCase() === 'true';
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
function systemImageArchCandidates() {
|
|
107
123
|
const configured = parseCsvEnv(process.env.ANDROID_SYSTEM_IMAGE_ARCH);
|
|
108
124
|
if (configured.length > 0) {
|
|
@@ -399,6 +415,7 @@ class AndroidController {
|
|
|
399
415
|
this.cli = new CLIExecutor();
|
|
400
416
|
this.avdName = readState().avdName || DEFAULT_AVD_NAME;
|
|
401
417
|
this.bootstrapPromise = null;
|
|
418
|
+
this.startPromise = null;
|
|
402
419
|
this.#registerProcessCleanup();
|
|
403
420
|
}
|
|
404
421
|
|
|
@@ -465,7 +482,12 @@ class AndroidController {
|
|
|
465
482
|
}
|
|
466
483
|
|
|
467
484
|
async ensureBootstrapped() {
|
|
468
|
-
|
|
485
|
+
const binariesReady =
|
|
486
|
+
isExecutable(adbBinary()) &&
|
|
487
|
+
isExecutable(sdkManagerBinary()) &&
|
|
488
|
+
isExecutable(emulatorBinary());
|
|
489
|
+
|
|
490
|
+
if (!binariesReady) {
|
|
469
491
|
if (this.bootstrapPromise) {
|
|
470
492
|
await this.bootstrapPromise;
|
|
471
493
|
} else {
|
|
@@ -478,16 +500,25 @@ class AndroidController {
|
|
|
478
500
|
}
|
|
479
501
|
}
|
|
480
502
|
|
|
503
|
+
const state = readState();
|
|
504
|
+
if (
|
|
505
|
+
state.bootstrapped === true &&
|
|
506
|
+
state.systemImage &&
|
|
507
|
+
!shouldForceSdkRefresh()
|
|
508
|
+
) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
481
512
|
appendState({ bootstrapped: true });
|
|
482
513
|
const sdkmanager = sdkManagerBinary();
|
|
483
514
|
const available = await this.#run(`${quoteShell(sdkmanager)} --sdk_root=${quoteShell(SDK_ROOT)} --list`, { timeout: 300000 });
|
|
484
515
|
const latestSystemImage = chooseConfiguredSystemImage(available) || chooseLatestSystemImage(available);
|
|
485
516
|
if (!latestSystemImage) throw new Error(formatSystemImageError(available));
|
|
486
517
|
|
|
487
|
-
const
|
|
488
|
-
const currentApiLevel = parseApiLevelFromSystemImage(
|
|
518
|
+
const refreshedState = readState();
|
|
519
|
+
const currentApiLevel = parseApiLevelFromSystemImage(refreshedState.systemImage);
|
|
489
520
|
const shouldUpgrade =
|
|
490
|
-
|
|
521
|
+
refreshedState.systemImage !== latestSystemImage.packageName ||
|
|
491
522
|
currentApiLevel < latestSystemImage.apiLevel;
|
|
492
523
|
|
|
493
524
|
if (shouldUpgrade) {
|
|
@@ -583,8 +614,13 @@ class AndroidController {
|
|
|
583
614
|
fs.writeFileSync(configPath, content);
|
|
584
615
|
}
|
|
585
616
|
|
|
586
|
-
async listDevices() {
|
|
587
|
-
|
|
617
|
+
async listDevices(options = {}) {
|
|
618
|
+
if (options.ensureBootstrapped !== false) {
|
|
619
|
+
await this.ensureBootstrapped();
|
|
620
|
+
}
|
|
621
|
+
if (!isExecutable(adbBinary())) {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
588
624
|
const out = await this.#run(`${quoteShell(adbBinary())} devices -l`);
|
|
589
625
|
const lines = out.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
590
626
|
return lines
|
|
@@ -600,9 +636,9 @@ class AndroidController {
|
|
|
600
636
|
});
|
|
601
637
|
}
|
|
602
638
|
|
|
603
|
-
async getPrimarySerial() {
|
|
639
|
+
async getPrimarySerial(options = {}) {
|
|
604
640
|
const state = readState();
|
|
605
|
-
const devices = await this.listDevices();
|
|
641
|
+
const devices = await this.listDevices(options);
|
|
606
642
|
const preferred = state.serial ? devices.find((device) => device.serial === state.serial && device.status === 'device') : null;
|
|
607
643
|
if (preferred) return preferred.serial;
|
|
608
644
|
const emulator = devices.find((device) => device.emulator && device.status === 'device');
|
|
@@ -615,11 +651,30 @@ class AndroidController {
|
|
|
615
651
|
return null;
|
|
616
652
|
}
|
|
617
653
|
|
|
618
|
-
async
|
|
654
|
+
async #startEmulatorBlocking(options = {}) {
|
|
655
|
+
appendState({
|
|
656
|
+
starting: true,
|
|
657
|
+
startupPhase: 'Preparing Android runtime',
|
|
658
|
+
lastStartError: null,
|
|
659
|
+
startRequestedAt: readState().startRequestedAt || new Date().toISOString(),
|
|
660
|
+
});
|
|
661
|
+
console.log('[Android] Preparing emulator start');
|
|
619
662
|
await this.ensureAvd();
|
|
663
|
+
appendState({
|
|
664
|
+
starting: true,
|
|
665
|
+
startupPhase: 'Checking for an existing Android device',
|
|
666
|
+
lastStartError: null,
|
|
667
|
+
});
|
|
620
668
|
this.#normalizeAvdConfig();
|
|
621
669
|
const serial = await this.getPrimarySerial();
|
|
622
670
|
if (serial) {
|
|
671
|
+
appendState({
|
|
672
|
+
starting: false,
|
|
673
|
+
startupPhase: null,
|
|
674
|
+
serial,
|
|
675
|
+
lastStartError: null,
|
|
676
|
+
lastLogLine: 'Android device already running.',
|
|
677
|
+
});
|
|
623
678
|
return {
|
|
624
679
|
success: true,
|
|
625
680
|
serial,
|
|
@@ -650,12 +705,59 @@ class AndroidController {
|
|
|
650
705
|
stdio: ['ignore', out, out],
|
|
651
706
|
env: sdkEnv(),
|
|
652
707
|
});
|
|
708
|
+
|
|
709
|
+
console.log(`[Android] Emulator process started (pid ${child.pid})`);
|
|
710
|
+
appendState({
|
|
711
|
+
emulatorPid: child.pid,
|
|
712
|
+
avdName: this.avdName,
|
|
713
|
+
logPath,
|
|
714
|
+
starting: true,
|
|
715
|
+
startupPhase: 'Waiting for Android emulator to boot',
|
|
716
|
+
lastStartError: null,
|
|
717
|
+
lastLogLine: 'Android emulator process started. Waiting for boot completion...',
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const processExit = new Promise((resolve) => {
|
|
721
|
+
child.once('exit', (code, signal) => {
|
|
722
|
+
resolve({ code, signal });
|
|
723
|
+
});
|
|
724
|
+
child.once('error', (error) => {
|
|
725
|
+
resolve({ code: null, signal: null, error });
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
653
729
|
child.unref();
|
|
654
730
|
|
|
655
|
-
|
|
731
|
+
const bootResult = await Promise.race([
|
|
732
|
+
this.waitForDevice({ timeoutMs: options.timeoutMs || 240000 }).then((serial) => ({
|
|
733
|
+
serial,
|
|
734
|
+
exited: false,
|
|
735
|
+
})),
|
|
736
|
+
processExit.then((result) => ({
|
|
737
|
+
exited: true,
|
|
738
|
+
...result,
|
|
739
|
+
})),
|
|
740
|
+
]);
|
|
741
|
+
|
|
742
|
+
if (bootResult.exited) {
|
|
743
|
+
const recentLogLines = tailFile(logPath, 12);
|
|
744
|
+
const lastLine =
|
|
745
|
+
bootResult.error?.message ||
|
|
746
|
+
recentLogLines[recentLogLines.length - 1] ||
|
|
747
|
+
`Emulator process exited before boot completed (code ${bootResult.code ?? 'unknown'}, signal ${bootResult.signal ?? 'none'}).`;
|
|
748
|
+
throw new Error(lastLine);
|
|
749
|
+
}
|
|
656
750
|
|
|
657
|
-
const onlineSerial =
|
|
658
|
-
appendState({
|
|
751
|
+
const onlineSerial = bootResult.serial;
|
|
752
|
+
appendState({
|
|
753
|
+
serial: onlineSerial,
|
|
754
|
+
emulatorPid: child.pid,
|
|
755
|
+
starting: false,
|
|
756
|
+
startupPhase: null,
|
|
757
|
+
lastStartError: null,
|
|
758
|
+
lastLogLine: 'Android emulator boot completed.',
|
|
759
|
+
});
|
|
760
|
+
console.log(`[Android] Emulator ready on ${onlineSerial}`);
|
|
659
761
|
|
|
660
762
|
return {
|
|
661
763
|
success: true,
|
|
@@ -665,6 +767,85 @@ class AndroidController {
|
|
|
665
767
|
};
|
|
666
768
|
}
|
|
667
769
|
|
|
770
|
+
async startEmulator(options = {}) {
|
|
771
|
+
if (this.startPromise) {
|
|
772
|
+
await this.startPromise;
|
|
773
|
+
const serial = await this.getPrimarySerial();
|
|
774
|
+
if (!serial) {
|
|
775
|
+
throw new Error(readState().lastStartError || 'Android emulator did not finish starting.');
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
success: true,
|
|
779
|
+
serial,
|
|
780
|
+
reused: false,
|
|
781
|
+
bootstrapped: readState().bootstrapped === true,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return this.#startEmulatorBlocking(options);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async requestStartEmulator(options = {}) {
|
|
789
|
+
const serial = await this.getPrimarySerial({ ensureBootstrapped: false }).catch(() => null);
|
|
790
|
+
if (serial) {
|
|
791
|
+
appendState({
|
|
792
|
+
starting: false,
|
|
793
|
+
startupPhase: null,
|
|
794
|
+
serial,
|
|
795
|
+
lastStartError: null,
|
|
796
|
+
lastLogLine: 'Android device already running.',
|
|
797
|
+
});
|
|
798
|
+
return {
|
|
799
|
+
success: true,
|
|
800
|
+
pending: false,
|
|
801
|
+
serial,
|
|
802
|
+
reused: true,
|
|
803
|
+
bootstrapped: readState().bootstrapped === true,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (!this.startPromise) {
|
|
808
|
+
const requestedAt = new Date().toISOString();
|
|
809
|
+
appendState({
|
|
810
|
+
starting: true,
|
|
811
|
+
startupPhase: 'Preparing Android runtime',
|
|
812
|
+
lastStartError: null,
|
|
813
|
+
startRequestedAt: requestedAt,
|
|
814
|
+
lastLogLine: 'Android start requested.',
|
|
815
|
+
});
|
|
816
|
+
const startPromise = this.#startEmulatorBlocking(options).catch((err) => {
|
|
817
|
+
const state = readState();
|
|
818
|
+
const recentLogLines = state.logPath ? tailFile(state.logPath, 12) : [];
|
|
819
|
+
const detailedMessage = recentLogLines[recentLogLines.length - 1] || err.message;
|
|
820
|
+
appendState({
|
|
821
|
+
starting: false,
|
|
822
|
+
startupPhase: 'Start failed',
|
|
823
|
+
lastStartError: detailedMessage,
|
|
824
|
+
lastLogLine: detailedMessage,
|
|
825
|
+
});
|
|
826
|
+
console.error('[Android] Emulator start failed:', detailedMessage);
|
|
827
|
+
throw new Error(detailedMessage);
|
|
828
|
+
}).finally(() => {
|
|
829
|
+
if (this.startPromise === startPromise) {
|
|
830
|
+
this.startPromise = null;
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
this.startPromise = startPromise;
|
|
834
|
+
startPromise.catch(() => {});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const state = readState();
|
|
838
|
+
return {
|
|
839
|
+
success: true,
|
|
840
|
+
pending: true,
|
|
841
|
+
bootstrapped: state.bootstrapped === true,
|
|
842
|
+
starting: true,
|
|
843
|
+
startupPhase: state.startupPhase || 'Preparing Android runtime',
|
|
844
|
+
startRequestedAt: state.startRequestedAt || null,
|
|
845
|
+
logPath: state.logPath || null,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
668
849
|
async waitForDevice(options = {}) {
|
|
669
850
|
const timeoutMs = Math.max(10000, Number(options.timeoutMs) || 180000);
|
|
670
851
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -699,7 +880,14 @@ class AndroidController {
|
|
|
699
880
|
if (state.emulatorPid) {
|
|
700
881
|
try { process.kill(state.emulatorPid, 'SIGTERM'); } catch {}
|
|
701
882
|
}
|
|
702
|
-
appendState({
|
|
883
|
+
appendState({
|
|
884
|
+
serial: null,
|
|
885
|
+
emulatorPid: null,
|
|
886
|
+
starting: false,
|
|
887
|
+
startupPhase: null,
|
|
888
|
+
lastStartError: null,
|
|
889
|
+
lastLogLine: 'Android emulator stopped.',
|
|
890
|
+
});
|
|
703
891
|
|
|
704
892
|
const deadline = Date.now() + 30000;
|
|
705
893
|
while (Date.now() < deadline) {
|
|
@@ -980,24 +1168,39 @@ class AndroidController {
|
|
|
980
1168
|
}
|
|
981
1169
|
|
|
982
1170
|
async getStatus() {
|
|
983
|
-
const devices = isExecutable(adbBinary())
|
|
1171
|
+
const devices = isExecutable(adbBinary())
|
|
1172
|
+
? await this.listDevices({ ensureBootstrapped: false }).catch(() => [])
|
|
1173
|
+
: [];
|
|
984
1174
|
const state = readState();
|
|
985
|
-
let lastLogLine =
|
|
1175
|
+
let lastLogLine =
|
|
1176
|
+
state.lastStartError ||
|
|
1177
|
+
state.lastLogLine ||
|
|
1178
|
+
state.startupPhase ||
|
|
1179
|
+
null;
|
|
986
1180
|
if (state.logPath && fs.existsSync(state.logPath)) {
|
|
987
1181
|
try {
|
|
988
1182
|
const lines = fs.readFileSync(state.logPath, 'utf8')
|
|
989
1183
|
.split('\n')
|
|
990
1184
|
.map((line) => line.trim())
|
|
991
1185
|
.filter(Boolean);
|
|
992
|
-
|
|
1186
|
+
const emulatorLogLine = [...lines].reverse().find((line) =>
|
|
993
1187
|
/fatal|error|warning|boot completed|disk space|running avd/i.test(line)
|
|
994
1188
|
) || lines[lines.length - 1] || null;
|
|
1189
|
+
lastLogLine = emulatorLogLine || lastLogLine;
|
|
995
1190
|
} catch {
|
|
996
|
-
lastLogLine =
|
|
1191
|
+
lastLogLine =
|
|
1192
|
+
state.lastStartError ||
|
|
1193
|
+
state.lastLogLine ||
|
|
1194
|
+
state.startupPhase ||
|
|
1195
|
+
null;
|
|
997
1196
|
}
|
|
998
1197
|
}
|
|
999
1198
|
return {
|
|
1000
1199
|
bootstrapped: state.bootstrapped === true,
|
|
1200
|
+
starting: state.starting === true || this.startPromise != null,
|
|
1201
|
+
startupPhase: state.startupPhase || null,
|
|
1202
|
+
startRequestedAt: state.startRequestedAt || null,
|
|
1203
|
+
lastStartError: state.lastStartError || null,
|
|
1001
1204
|
sdkRoot: SDK_ROOT,
|
|
1002
1205
|
avdHome: AVD_HOME,
|
|
1003
1206
|
avdName: this.avdName,
|