|
8 | 8 | using System.IO; |
9 | 9 | using System.Linq; |
10 | 10 | using System.Runtime.InteropServices; |
| 11 | +using System.Text; |
11 | 12 | using System.Threading; |
12 | 13 | using Microsoft.DotNet.XHarness.Android.Execution; |
13 | 14 | using Microsoft.Extensions.Logging; |
@@ -296,6 +297,220 @@ public void StartAdbServer() |
296 | 297 |
|
297 | 298 | public void KillAdbServer() => RunAdbCommand(new[] { "kill-server" }).ThrowIfFailed("Error killing ADB Server"); |
298 | 299 |
|
| 300 | + /// <summary> |
| 301 | + /// Attempts to recover the Android emulator when no suitable device is found. |
| 302 | + /// Steps: |
| 303 | + /// 1. Logs diagnostics about currently available devices |
| 304 | + /// 2. Resets the ADB daemon (kill-server + start-server) |
| 305 | + /// 3. If device still not visible, tries to restart the emulator process via |
| 306 | + /// systemctl (on Linux/systemd machines) or 'adb emu restart' as a fallback |
| 307 | + /// 4. Waits for the device to appear and complete boot |
| 308 | + /// </summary> |
| 309 | + /// <returns>true if a device appeared after recovery, false otherwise</returns> |
| 310 | + public bool TryRecoverEmulator() |
| 311 | + { |
| 312 | + // Step 1: Diagnostics - log what devices ARE currently attached |
| 313 | + _log.LogInformation("Attempting emulator recovery. Logging available devices for diagnostics..."); |
| 314 | + var devicesResult = RunAdbCommand(new[] { "devices", "-l" }, TimeSpan.FromSeconds(30)); |
| 315 | + _log.LogInformation($"Current 'adb devices -l' output:{Environment.NewLine}{devicesResult.StandardOutput}"); |
| 316 | + |
| 317 | + // On Linux, also log the emulator service status for diagnostics |
| 318 | + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) |
| 319 | + { |
| 320 | + try |
| 321 | + { |
| 322 | + var statusResult = RunSystemCommand("systemctl", new[] { "status", "android-emulator" }, TimeSpan.FromSeconds(15)); |
| 323 | + _log.LogInformation($"systemctl status android-emulator:{Environment.NewLine}{statusResult.StandardOutput}"); |
| 324 | + } |
| 325 | + catch (Exception e) |
| 326 | + { |
| 327 | + _log.LogDebug($"Unable to check systemctl status: {e.Message}"); |
| 328 | + } |
| 329 | + } |
| 330 | + |
| 331 | + // Step 2: Reset ADB daemon (kill-server + start-server) |
| 332 | + _log.LogInformation("Resetting ADB server as part of emulator recovery..."); |
| 333 | + try |
| 334 | + { |
| 335 | + KillAdbServer(); |
| 336 | + } |
| 337 | + catch (Exception e) |
| 338 | + { |
| 339 | + _log.LogWarning($"Error killing ADB server during recovery: {e.Message}"); |
| 340 | + } |
| 341 | + |
| 342 | + Thread.Sleep(TimeSpan.FromSeconds(2)); |
| 343 | + |
| 344 | + try |
| 345 | + { |
| 346 | + StartAdbServer(); |
| 347 | + } |
| 348 | + catch (Exception e) |
| 349 | + { |
| 350 | + _log.LogWarning($"Error restarting ADB server during recovery: {e.Message}"); |
| 351 | + } |
| 352 | + |
| 353 | + // Step 3: Re-check for devices after ADB reset - if a device reappeared, we are done |
| 354 | + var recheckResult = RunAdbCommand(new[] { "devices", "-l" }, TimeSpan.FromSeconds(30)); |
| 355 | + _log.LogInformation($"Devices after ADB server reset:{Environment.NewLine}{recheckResult.StandardOutput}"); |
| 356 | + |
| 357 | + var recheckLines = recheckResult.StandardOutput.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); |
| 358 | + if (recheckLines.Length >= 2) |
| 359 | + { |
| 360 | + _log.LogInformation("Device(s) reappeared after ADB server reset; skipping emulator service restart"); |
| 361 | + return true; |
| 362 | + } |
| 363 | + |
| 364 | + // Step 4: Try to restart the emulator process |
| 365 | + bool restartAttempted = false; |
| 366 | + |
| 367 | + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) |
| 368 | + { |
| 369 | + try |
| 370 | + { |
| 371 | + _log.LogInformation("Attempting to restart android-emulator service via systemctl..."); |
| 372 | + var restartResult = RunSystemCommand("systemctl", new[] { "restart", "android-emulator" }, TimeSpan.FromMinutes(2)); |
| 373 | + if (restartResult.Succeeded) |
| 374 | + { |
| 375 | + _log.LogInformation("systemctl restart android-emulator succeeded"); |
| 376 | + restartAttempted = true; |
| 377 | + } |
| 378 | + else |
| 379 | + { |
| 380 | + _log.LogWarning($"systemctl restart android-emulator failed (exit code {restartResult.ExitCode}):{Environment.NewLine}{restartResult.StandardError}"); |
| 381 | + } |
| 382 | + } |
| 383 | + catch (Exception e) |
| 384 | + { |
| 385 | + _log.LogDebug($"Unable to restart via systemctl: {e.Message}"); |
| 386 | + } |
| 387 | + } |
| 388 | + |
| 389 | + if (!restartAttempted) |
| 390 | + { |
| 391 | + // Fallback: attempt 'adb emu restart' (works if emulator console port is reachable) |
| 392 | + _log.LogInformation("Attempting 'adb emu restart' as emulator restart fallback..."); |
| 393 | + try |
| 394 | + { |
| 395 | + var emuResult = RunAdbCommand(new[] { "emu", "restart" }, TimeSpan.FromSeconds(30)); |
| 396 | + _log.LogDebug($"adb emu restart output: {emuResult.StandardOutput}"); |
| 397 | + restartAttempted = true; |
| 398 | + } |
| 399 | + catch (Exception e) |
| 400 | + { |
| 401 | + _log.LogWarning($"adb emu restart failed: {e.Message}"); |
| 402 | + } |
| 403 | + } |
| 404 | + |
| 405 | + if (!restartAttempted) |
| 406 | + { |
| 407 | + _log.LogWarning("No emulator restart method succeeded; recovery not possible"); |
| 408 | + return false; |
| 409 | + } |
| 410 | + |
| 411 | + // Step 5: Wait for a device to reappear |
| 412 | + _log.LogInformation("Waiting for emulator to reappear after recovery (max 5 minutes)..."); |
| 413 | + bool deviceAppeared = Retry( |
| 414 | + () => |
| 415 | + { |
| 416 | + var r = RunAdbCommand(new[] { "devices", "-l" }, TimeSpan.FromSeconds(30)); |
| 417 | + var lines = r.StandardOutput.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); |
| 418 | + return lines.Length >= 2; |
| 419 | + }, |
| 420 | + retryInterval: TimeSpan.FromSeconds(10), |
| 421 | + retryPeriod: TimeSpan.FromMinutes(5)); |
| 422 | + |
| 423 | + if (!deviceAppeared) |
| 424 | + { |
| 425 | + _log.LogWarning("No device appeared after emulator recovery attempt"); |
| 426 | + return false; |
| 427 | + } |
| 428 | + |
| 429 | + // Step 6: Wait for boot completion |
| 430 | + _log.LogInformation("Waiting for emulator boot completion after recovery..."); |
| 431 | + var bootWatch = Stopwatch.StartNew(); |
| 432 | + bool bootCompleted = Retry( |
| 433 | + () => |
| 434 | + { |
| 435 | + string? bootStatus = GetDeviceProperty(AdbProperty.BootCompletion, null); |
| 436 | + _log.LogDebug($"sys.boot_completed = '{bootStatus}'"); |
| 437 | + return bootStatus?.StartsWith('1') ?? false; |
| 438 | + }, |
| 439 | + retryInterval: TimeSpan.FromSeconds(10), |
| 440 | + retryPeriod: TimeToWaitForBootCompletion); |
| 441 | + |
| 442 | + if (bootCompleted) |
| 443 | + { |
| 444 | + _log.LogInformation($"Emulator recovered and boot completed after {(int)bootWatch.Elapsed.TotalSeconds} seconds"); |
| 445 | + return true; |
| 446 | + } |
| 447 | + else |
| 448 | + { |
| 449 | + _log.LogWarning("Emulator appeared but did not complete boot within the timeout after recovery"); |
| 450 | + return false; |
| 451 | + } |
| 452 | + } |
| 453 | + |
| 454 | + /// <summary> |
| 455 | + /// Runs a non-ADB system command (e.g. systemctl) and returns the result. |
| 456 | + /// </summary> |
| 457 | + private static ProcessExecutionResults RunSystemCommand(string command, IEnumerable<string> arguments, TimeSpan timeout) |
| 458 | + { |
| 459 | + var processStartInfo = new ProcessStartInfo() |
| 460 | + { |
| 461 | + CreateNoWindow = true, |
| 462 | + UseShellExecute = false, |
| 463 | + RedirectStandardOutput = true, |
| 464 | + RedirectStandardError = true, |
| 465 | + FileName = command, |
| 466 | + }; |
| 467 | + |
| 468 | + foreach (var arg in arguments) |
| 469 | + { |
| 470 | + processStartInfo.ArgumentList.Add(arg); |
| 471 | + } |
| 472 | + |
| 473 | + var p = new Process() { StartInfo = processStartInfo }; |
| 474 | + var standardOut = new StringBuilder(); |
| 475 | + var standardErr = new StringBuilder(); |
| 476 | + |
| 477 | + p.OutputDataReceived += (sender, e) => { if (e.Data != null) lock (standardOut) standardOut.AppendLine(e.Data); }; |
| 478 | + p.ErrorDataReceived += (sender, e) => { if (e.Data != null) lock (standardErr) standardErr.AppendLine(e.Data); }; |
| 479 | + |
| 480 | + p.Start(); |
| 481 | + p.BeginOutputReadLine(); |
| 482 | + p.BeginErrorReadLine(); |
| 483 | + |
| 484 | + bool timedOut = false; |
| 485 | + int exitCode; |
| 486 | + |
| 487 | + if (!p.WaitForExit((int)Math.Min(timeout.TotalMilliseconds, int.MaxValue))) |
| 488 | + { |
| 489 | + timedOut = true; |
| 490 | + exitCode = -1; |
| 491 | + try { p.Kill(); } catch { } |
| 492 | + } |
| 493 | + else |
| 494 | + { |
| 495 | + p.WaitForExit(); |
| 496 | + exitCode = p.ExitCode; |
| 497 | + } |
| 498 | + |
| 499 | + p.Close(); |
| 500 | + |
| 501 | + lock (standardOut) |
| 502 | + lock (standardErr) |
| 503 | + { |
| 504 | + return new ProcessExecutionResults() |
| 505 | + { |
| 506 | + ExitCode = exitCode, |
| 507 | + StandardOutput = standardOut.ToString(), |
| 508 | + StandardError = standardErr.ToString(), |
| 509 | + TimedOut = timedOut, |
| 510 | + }; |
| 511 | + } |
| 512 | + } |
| 513 | + |
299 | 514 | public int CopyHeadlessFolder(string testPath, bool sharedRuntime = false) |
300 | 515 | { |
301 | 516 | _log.LogInformation($"Attempting to install {testPath}"); |
@@ -776,11 +991,15 @@ private IReadOnlyCollection<AndroidDevice> GetAllDevices( |
776 | 991 |
|
777 | 992 | if (requiredArchitectures?.Any() ?? false) |
778 | 993 | { |
| 994 | + var availableDevices = devices; |
779 | 995 | devices = devices.Where(device => device.SupportedArchitectures?.Intersect(requiredArchitectures).Any() ?? false).ToList(); |
780 | 996 |
|
781 | 997 | if (devices.Count == 0) |
782 | 998 | { |
783 | | - _log.LogError($"No attached device supports one of required architectures {string.Join(", ", requiredArchitectures)}"); |
| 999 | + var availableArchInfo = string.Join(", ", availableDevices.Select( |
| 1000 | + d => $"{d.DeviceSerial}=[{string.Join(",", d.SupportedArchitectures ?? Array.Empty<string>())}]")); |
| 1001 | + _log.LogError($"No attached device supports one of required architectures: {string.Join(", ", requiredArchitectures)}. " + |
| 1002 | + $"Found {availableDevices.Count} device(s) with: {availableArchInfo}"); |
784 | 1003 | return devices; |
785 | 1004 | } |
786 | 1005 | } |
|
0 commit comments