1919import static com .google .common .util .concurrent .Futures .immediateFailedFuture ;
2020import static com .google .common .util .concurrent .MoreExecutors .directExecutor ;
2121
22+ import com .google .common .flogger .FluentLogger ;
23+ import com .google .common .util .concurrent .FluentFuture ;
2224import com .google .common .util .concurrent .Futures ;
2325import com .google .common .util .concurrent .ListenableFuture ;
24- import com .google .devtools .mobileharness .api .model .error .BasicErrorId ;
25- import com .google .devtools .mobileharness .api .model .error .MobileHarnessException ;
2626import com .google .devtools .mobileharness .api .model .proto .Device .DeviceStatus ;
2727import com .google .devtools .mobileharness .api .query .proto .FilterProto ;
2828import com .google .devtools .mobileharness .api .query .proto .LabQueryProto .LabQuery ;
2929import com .google .devtools .mobileharness .fe .v6 .service .device .provider .DeviceOpsStubProvider ;
30+ import com .google .devtools .mobileharness .fe .v6 .service .errors .FeServiceException ;
3031import com .google .devtools .mobileharness .fe .v6 .service .host .handlers .TroubleshootScriptRegistry .ScriptMetadata ;
3132import com .google .devtools .mobileharness .fe .v6 .service .proto .host .ListTroubleshootScriptsRequest ;
3233import com .google .devtools .mobileharness .fe .v6 .service .proto .host .ListTroubleshootScriptsResponse ;
4041import com .google .devtools .mobileharness .shared .labinfo .proto .LabInfoServiceProto .GetLabInfoResponse ;
4142import com .google .wireless .qa .mobileharness .lab .proto .DeviceOpsServ ;
4243import com .google .wireless .qa .mobileharness .lab .proto .DeviceOpsServ .RunTroubleshootScriptRequest .TroubleshootScript ;
44+ import io .grpc .Status ;
45+ import io .grpc .StatusRuntimeException ;
4346import javax .inject .Inject ;
4447import javax .inject .Singleton ;
4548
4649/** Handler for troubleshoot script operations (run and list). */
4750@ Singleton
4851public class TroubleshootScriptHandler {
4952
53+ private static final FluentLogger logger = FluentLogger .forEnclosingClass ();
54+
5055 private final LabInfoProvider labInfoProvider ;
5156 private final DeviceOpsStubProvider deviceOpsStubProvider ;
5257
@@ -62,47 +67,55 @@ public ListenableFuture<RunTroubleshootScriptResponse> runTroubleshootScript(
6267 RunTroubleshootScriptRequest request , UniverseScope universe ) {
6368 if (!(universe instanceof UniverseScope .SelfUniverse )) {
6469 return immediateFailedFuture (
65- new IllegalArgumentException (
70+ FeServiceException . unimplemented (
6671 "Troubleshoot scripts are only supported in the self universe." ));
6772 }
6873 String hostName = request .getHostName ();
6974
70- return Futures .transformAsync (
71- labInfoProvider .getLabInfoAsync (createGetLabInfoRequest (hostName ), universe ),
72- labInfoResponse -> {
73- DeviceOpsServ .RunTroubleshootScriptRequest .TroubleshootScript labScript =
74- switch (request .getScript ()) {
75- case RESET_USB_HUB ->
76- DeviceOpsServ .RunTroubleshootScriptRequest .TroubleshootScript .RESET_USB_HUB ;
77- default ->
78- throw new MobileHarnessException (
79- BasicErrorId .COMMAND_EXEC_FAIL ,
80- "Unsupported troubleshoot script: " + request .getScript ());
81- };
82-
83- DeviceOpsServ .RunTroubleshootScriptRequest labRequest =
84- DeviceOpsServ .RunTroubleshootScriptRequest .newBuilder ().setScript (labScript ).build ();
85-
86- DeviceOpsStub stub = deviceOpsStubProvider .createStub (hostName , universe );
87- return Futures .transform (
88- stub .runTroubleshootScriptAsync (labRequest ),
89- labResponse ->
90- RunTroubleshootScriptResponse .newBuilder ()
91- .setExitCode (labResponse .getExitCode ())
92- .setStdout (labResponse .getStdout ())
93- .setStderr (labResponse .getStderr ())
94- .build (),
95- directExecutor ());
96- },
97- directExecutor ());
75+ return FluentFuture .from (
76+ labInfoProvider .getLabInfoAsync (createGetLabInfoRequest (hostName ), universe ))
77+ .transformAsync (
78+ labInfoResponse -> {
79+ DeviceOpsServ .RunTroubleshootScriptRequest .TroubleshootScript labScript =
80+ switch (request .getScript ()) {
81+ case RESET_USB_HUB ->
82+ DeviceOpsServ .RunTroubleshootScriptRequest .TroubleshootScript .RESET_USB_HUB ;
83+ default ->
84+ throw FeServiceException .invalidArgument (
85+ "Unsupported troubleshoot script: " + request .getScript ());
86+ };
87+
88+ DeviceOpsServ .RunTroubleshootScriptRequest labRequest =
89+ DeviceOpsServ .RunTroubleshootScriptRequest .newBuilder ()
90+ .setScript (labScript )
91+ .build ();
92+
93+ DeviceOpsStub stub = deviceOpsStubProvider .createStub (hostName , universe );
94+ return Futures .transform (
95+ stub .runTroubleshootScriptAsync (labRequest ),
96+ labResponse ->
97+ RunTroubleshootScriptResponse .newBuilder ()
98+ .setExitCode (labResponse .getExitCode ())
99+ .setStdout (labResponse .getStdout ())
100+ .setStderr (labResponse .getStderr ())
101+ .build (),
102+ directExecutor ());
103+ },
104+ directExecutor ())
105+ .catching (
106+ Exception .class ,
107+ e -> {
108+ throw toFeServiceException (hostName , e );
109+ },
110+ directExecutor ());
98111 }
99112
100113 /** Lists available troubleshoot scripts, disabling them if any device is BUSY. */
101114 public ListenableFuture <ListTroubleshootScriptsResponse > listTroubleshootScripts (
102115 ListTroubleshootScriptsRequest request , UniverseScope universe ) {
103116 if (!(universe instanceof UniverseScope .SelfUniverse )) {
104117 return immediateFailedFuture (
105- new IllegalArgumentException (
118+ FeServiceException . unimplemented (
106119 "Troubleshoot scripts are only supported in the self universe." ));
107120 }
108121 String hostName = request .getHostName ();
@@ -151,6 +164,37 @@ record Precondition(boolean canRun, String disabledReason) {}
151164 directExecutor ());
152165 }
153166
167+ /**
168+ * Converts downstream exceptions into {@link FeServiceException} so that the gRPC/AF boundary
169+ * (FeGrpcInvoker / SafeExceptionHandler) maps them to the correct HTTP status code.
170+ *
171+ * <p>If the exception is already a {@code FeServiceException} (e.g. from validation above), it is
172+ * re-thrown as-is. gRPC {@link StatusRuntimeException}s from the lab server RPC preserve their
173+ * original code. All other exceptions become {@code INTERNAL}.
174+ */
175+ private static FeServiceException toFeServiceException (String hostName , Exception e ) {
176+ if (e instanceof FeServiceException feEx ) {
177+ return feEx ;
178+ }
179+ if (e instanceof StatusRuntimeException sre ) {
180+ Status .Code code = sre .getStatus ().getCode ();
181+ String description = sre .getStatus ().getDescription ();
182+ String message =
183+ String .format (
184+ "Troubleshoot script failed on host %s: %s%s" ,
185+ hostName , code , description != null ? " — " + description : "" );
186+ return new FeServiceException (code , message , sre );
187+ }
188+ logger .atWarning ().withCause (e ).log (
189+ "Unexpected error running troubleshoot script on host %s" , hostName );
190+ return new FeServiceException (
191+ Status .Code .INTERNAL ,
192+ String .format (
193+ "Troubleshoot script failed on host %s: %s" ,
194+ hostName , e .getMessage () != null ? e .getMessage () : e .getClass ().getSimpleName ()),
195+ e );
196+ }
197+
154198 /** Returns true if no devices are BUSY on the host, meaning it is safe to run RESET_USB_HUB. */
155199 private static boolean canRunResetUsbHub (GetLabInfoResponse labInfoResponse ) {
156200 return labInfoResponse .getLabQueryResult ().getLabView ().getLabDataList ().stream ()
0 commit comments