|
16 | 16 |
|
17 | 17 | package com.google.devtools.mobileharness.platform.android.shared.emulator; |
18 | 18 |
|
| 19 | +import static com.google.common.base.Strings.isNullOrEmpty; |
19 | 20 | import static com.google.common.base.Strings.nullToEmpty; |
20 | 21 | import static java.nio.charset.StandardCharsets.UTF_8; |
21 | 22 |
|
|
37 | 38 | import com.google.common.annotations.VisibleForTesting; |
38 | 39 | import com.google.common.base.Stopwatch; |
39 | 40 | import com.google.common.collect.ImmutableList; |
| 41 | +import com.google.common.flogger.FluentLogger; |
40 | 42 | import com.google.common.io.Files; |
41 | 43 | import com.google.devtools.mobileharness.api.model.error.AndroidErrorId; |
42 | 44 | import com.google.devtools.mobileharness.api.model.error.BasicErrorId; |
|
56 | 58 | import com.google.devtools.mobileharness.shared.util.concurrent.retry.RetryException; |
57 | 59 | import com.google.devtools.mobileharness.shared.util.concurrent.retry.RetryStrategy; |
58 | 60 | import com.google.devtools.mobileharness.shared.util.concurrent.retry.RetryingCallable; |
| 61 | +import com.google.devtools.mobileharness.shared.util.file.local.LocalFileUtil; |
59 | 62 | import com.google.errorprone.annotations.CanIgnoreReturnValue; |
60 | 63 | import com.google.gson.stream.MalformedJsonException; |
61 | 64 | import java.io.File; |
| 65 | +import java.io.FileOutputStream; |
62 | 66 | import java.io.IOException; |
63 | 67 | import java.time.Duration; |
64 | 68 | import java.util.Base64; |
65 | 69 | import java.util.HashMap; |
66 | 70 | import java.util.List; |
67 | 71 | import java.util.Map; |
| 72 | +import java.util.regex.Matcher; |
| 73 | +import java.util.regex.Pattern; |
68 | 74 | import javax.annotation.Nullable; |
69 | 75 |
|
70 | 76 | /** Java client for Cloud Orchestrator API. */ |
71 | 77 | public class CloudOrchestratorClient { |
72 | 78 |
|
| 79 | + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
73 | 80 | private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); |
74 | 81 | private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); |
| 82 | + private static final Pattern LOG_LINK_PATTERN = |
| 83 | + Pattern.compile("<a[^>]*href=\"([^\"]+)\"[^>]*>", Pattern.CASE_INSENSITIVE); |
75 | 84 |
|
76 | 85 | private static final Duration DEFAULT_REQUEST_TIMEOUT = Duration.ofSeconds(30); |
77 | 86 | private static final Duration WAIT_OPERATION_MAX_TIMEOUT = Duration.ofMinutes(10); |
@@ -600,6 +609,139 @@ private <T> T put(String path, @Nullable Object body, Class<T> responseClass) |
600 | 609 | } |
601 | 610 | } |
602 | 611 |
|
| 612 | + /** |
| 613 | + * Downloads a file from the given path to the target file. |
| 614 | + * |
| 615 | + * @param path the path relative to the root endpoint |
| 616 | + * @param targetFile the file to save the content to |
| 617 | + * @throws MobileHarnessException if the download fails |
| 618 | + */ |
| 619 | + public void downloadFile(String path, File targetFile) throws MobileHarnessException { |
| 620 | + int maxRetries = 3; |
| 621 | + LocalFileUtil localFileUtil = new LocalFileUtil(); |
| 622 | + localFileUtil.prepareParentDir(targetFile.getAbsolutePath()); |
| 623 | + try { |
| 624 | + RetryingCallable.newBuilder( |
| 625 | + () -> { |
| 626 | + GenericUrl url = new GenericUrl(rootEndpoint + path); |
| 627 | + HttpRequest request = requestFactory.buildRequest("GET", url, null); |
| 628 | + prepareRequest(request); |
| 629 | + HttpResponse response = request.execute(); |
| 630 | + try (FileOutputStream out = new FileOutputStream(targetFile)) { |
| 631 | + response.download(out); |
| 632 | + } finally { |
| 633 | + response.disconnect(); |
| 634 | + } |
| 635 | + return null; |
| 636 | + }, |
| 637 | + RetryStrategy.exponentialBackoff(requestRetryDelay, 2, maxRetries)) |
| 638 | + .setPredicate(this::isRetriable) |
| 639 | + .build() |
| 640 | + .call(); |
| 641 | + } catch (RetryException e) { |
| 642 | + Throwable cause = e.getCause(); |
| 643 | + if (cause instanceof InterruptedException) { |
| 644 | + Thread.currentThread().interrupt(); |
| 645 | + throw new MobileHarnessException( |
| 646 | + BasicErrorId.LOCAL_NETWORK_ERROR, |
| 647 | + String.format("Interrupted while downloading file from %s", path), |
| 648 | + cause); |
| 649 | + } |
| 650 | + if (cause instanceof HttpResponseException httpResponseException) { |
| 651 | + throw handleHttpException( |
| 652 | + String.format("Failed to download file from %s", path), httpResponseException); |
| 653 | + } |
| 654 | + throw new MobileHarnessException( |
| 655 | + BasicErrorId.LOCAL_NETWORK_ERROR, |
| 656 | + String.format("Failed to download file from %s", path), |
| 657 | + e); |
| 658 | + } |
| 659 | + } |
| 660 | + |
| 661 | + /** |
| 662 | + * Fetches all logs from the logs directory and saves them to the target directory. |
| 663 | + * |
| 664 | + * @param hostId the ID of the host |
| 665 | + * @param cvdGroup the group of the CVD |
| 666 | + * @param cvdName the name of the CVD |
| 667 | + * @param targetDir the directory to save the logs to |
| 668 | + * @throws MobileHarnessException if the operation fails |
| 669 | + */ |
| 670 | + public void fetchAllLogs(String hostId, String cvdGroup, String cvdName, File targetDir) |
| 671 | + throws MobileHarnessException { |
| 672 | + String logsPath = "/hosts/" + hostId + "/cvds/" + cvdGroup + "/" + cvdName + "/logs/"; |
| 673 | + int maxRetries = 3; |
| 674 | + String htmlContent; |
| 675 | + try { |
| 676 | + htmlContent = |
| 677 | + RetryingCallable.newBuilder( |
| 678 | + () -> { |
| 679 | + GenericUrl url = new GenericUrl(rootEndpoint + logsPath); |
| 680 | + HttpRequest request = requestFactory.buildRequest("GET", url, null); |
| 681 | + prepareRequest(request); |
| 682 | + HttpResponse response = request.execute(); |
| 683 | + try { |
| 684 | + return response.parseAsString(); |
| 685 | + } finally { |
| 686 | + response.disconnect(); |
| 687 | + } |
| 688 | + }, |
| 689 | + RetryStrategy.exponentialBackoff(requestRetryDelay, 2, maxRetries)) |
| 690 | + .setPredicate(this::isRetriable) |
| 691 | + .build() |
| 692 | + .call(); |
| 693 | + } catch (RetryException e) { |
| 694 | + Throwable cause = e.getCause(); |
| 695 | + if (cause instanceof InterruptedException) { |
| 696 | + Thread.currentThread().interrupt(); |
| 697 | + throw new MobileHarnessException( |
| 698 | + BasicErrorId.LOCAL_NETWORK_ERROR, |
| 699 | + String.format("Interrupted while fetching logs directory listing from %s", logsPath), |
| 700 | + cause); |
| 701 | + } |
| 702 | + if (cause instanceof HttpResponseException httpResponseException) { |
| 703 | + throw handleHttpException( |
| 704 | + String.format("Failed to fetch logs directory listing from %s", logsPath), |
| 705 | + httpResponseException); |
| 706 | + } |
| 707 | + throw new MobileHarnessException( |
| 708 | + BasicErrorId.LOCAL_NETWORK_ERROR, |
| 709 | + String.format("Failed to fetch logs directory listing from %s", logsPath), |
| 710 | + e); |
| 711 | + } |
| 712 | + |
| 713 | + if (isNullOrEmpty(htmlContent)) { |
| 714 | + return; |
| 715 | + } |
| 716 | + |
| 717 | + // Parse HTML to find links |
| 718 | + Matcher matcher = LOG_LINK_PATTERN.matcher(htmlContent); |
| 719 | + |
| 720 | + while (matcher.find()) { |
| 721 | + String filename = matcher.group(1); |
| 722 | + if (filename.endsWith("/") || filename.equals("..")) { |
| 723 | + continue; |
| 724 | + } |
| 725 | + File targetFile = new File(targetDir, new File(filename).getName()); |
| 726 | + try { |
| 727 | + logger.atInfo().log( |
| 728 | + "Downloading log file %s to %s", filename, targetFile.getAbsolutePath()); |
| 729 | + downloadFile(logsPath + filename, targetFile); |
| 730 | + } catch (MobileHarnessException e) { |
| 731 | + logger.atWarning().withCause(e).log("Failed to download log file %s", filename); |
| 732 | + } |
| 733 | + } |
| 734 | + } |
| 735 | + |
| 736 | + private void prepareRequest(HttpRequest request) { |
| 737 | + if (sessionCookie != null) { |
| 738 | + request.getHeaders().set("Cookie", sessionCookie); |
| 739 | + } |
| 740 | + if (basicAuth != null) { |
| 741 | + request.getHeaders().set("Authorization", "Basic " + basicAuth); |
| 742 | + } |
| 743 | + } |
| 744 | + |
603 | 745 | private <T> T executeRequest( |
604 | 746 | String method, |
605 | 747 | String path, |
@@ -634,12 +776,7 @@ private <T> T executeRequest( |
634 | 776 | if (buildApiCreds != null) { |
635 | 777 | request.getHeaders().set("X-Cutf-Host-Orchestrator-BuildAPI-Creds", buildApiCreds); |
636 | 778 | } |
637 | | - if (sessionCookie != null) { |
638 | | - request.getHeaders().set("Cookie", sessionCookie); |
639 | | - } |
640 | | - if (basicAuth != null) { |
641 | | - request.getHeaders().set("Authorization", "Basic " + basicAuth); |
642 | | - } |
| 779 | + prepareRequest(request); |
643 | 780 | if (timeout != null) { |
644 | 781 | request.setConnectTimeout((int) timeout.toMillis()); |
645 | 782 | request.setReadTimeout((int) timeout.toMillis()); |
|
0 commit comments