1717package io .appium .java_client .service .local ;
1818
1919import com .google .common .annotations .VisibleForTesting ;
20+ import lombok .Getter ;
2021import lombok .SneakyThrows ;
21- import org .openqa .selenium .net .UrlChecker ;
2222import org .openqa .selenium .os .ExternalProcess ;
2323import org .openqa .selenium .remote .service .DriverService ;
2424import org .slf4j .Logger ;
3030import java .io .File ;
3131import java .io .IOException ;
3232import java .io .OutputStream ;
33- import java .net .MalformedURLException ;
3433import java .net .URL ;
3534import java .time .Duration ;
3635import java .util .ArrayList ;
3736import java .util .List ;
3837import java .util .Map ;
3938import java .util .Optional ;
40- import java .util .concurrent .TimeUnit ;
4139import java .util .concurrent .locks .ReentrantLock ;
4240import java .util .function .BiConsumer ;
4341import java .util .function .Consumer ;
@@ -67,7 +65,9 @@ public final class AppiumDriverLocalService extends DriverService {
6765 private final Duration startupTimeout ;
6866 private final ReentrantLock lock = new ReentrantLock (true ); //uses "fair" thread ordering policy
6967 private final ListOutputStream stream = new ListOutputStream ().add (System .out );
68+ private final AppiumServerAvailabilityChecker availabilityChecker = new AppiumServerAvailabilityChecker ();
7069 private final URL url ;
70+ @ Getter
7171 private String basePath ;
7272
7373 private ExternalProcess process = null ;
@@ -97,10 +97,6 @@ public AppiumDriverLocalService withBasePath(String basePath) {
9797 return this ;
9898 }
9999
100- public String getBasePath () {
101- return this .basePath ;
102- }
103-
104100 @ SneakyThrows
105101 private static URL addSuffix (URL url , String suffix ) {
106102 return url .toURI ().resolve ("." + (suffix .startsWith ("/" ) ? suffix : "/" + suffix )).toURL ();
@@ -131,36 +127,40 @@ public boolean isRunning() {
131127 }
132128
133129 try {
134- ping (IS_RUNNING_PING_TIMEOUT );
135- return true ;
136- } catch ( UrlChecker . TimeoutException e ) {
130+ return ping (IS_RUNNING_PING_TIMEOUT );
131+ } catch ( AppiumServerAvailabilityChecker . ConnectionTimeout
132+ | AppiumServerAvailabilityChecker . ConnectionError e ) {
137133 return false ;
138- } catch (MalformedURLException e ) {
139- throw new AppiumServerHasNotBeenStartedLocallyException ( e . getMessage (), e );
134+ } catch (InterruptedException e ) {
135+ throw new RuntimeException ( e );
140136 }
141137 } finally {
142138 lock .unlock ();
143139 }
140+ }
144141
142+ private boolean ping (Duration timeout ) throws InterruptedException {
143+ var baseURL = fixBroadcastAddresses (getUrl ());
144+ var statusUrl = addSuffix (baseURL , "/status" );
145+ return availabilityChecker .waitUntilAvailable (statusUrl , timeout );
145146 }
146147
147- private void ping (Duration timeout ) throws UrlChecker .TimeoutException , MalformedURLException {
148- URL baseURL = getUrl ();
149- String host = baseURL .getHost ();
148+ private URL fixBroadcastAddresses (URL url ) {
149+ var host = url .getHost ();
150150 // The operating system will block direct access to the universal broadcast IP address
151151 if (host .equals (BROADCAST_IP4_ADDRESS )) {
152- baseURL = replaceHost (baseURL , BROADCAST_IP4_ADDRESS , "127.0.0.1" );
153- } else if (host .equals (BROADCAST_IP6_ADDRESS )) {
154- baseURL = replaceHost (baseURL , BROADCAST_IP6_ADDRESS , "::1" );
152+ return replaceHost (url , BROADCAST_IP4_ADDRESS , "127.0.0.1" );
153+ }
154+ if (host .equals (BROADCAST_IP6_ADDRESS )) {
155+ return replaceHost (url , BROADCAST_IP6_ADDRESS , "::1" );
155156 }
156- URL status = addSuffix (baseURL , "/status" );
157- new UrlChecker ().waitUntilAvailable (timeout .toMillis (), TimeUnit .MILLISECONDS , status );
157+ return url ;
158158 }
159159
160160 /**
161161 * Starts the defined appium server.
162162 *
163- * @throws AppiumServerHasNotBeenStartedLocallyException If an error occurs while spawning the child process .
163+ * @throws AppiumServerHasNotBeenStartedLocallyException If an error occurs on Appium server startup .
164164 * @see #stop()
165165 */
166166 @ Override
@@ -172,40 +172,75 @@ public void start() throws AppiumServerHasNotBeenStartedLocallyException {
172172 }
173173
174174 try {
175- ExternalProcess . Builder processBuilder = ExternalProcess .builder ()
175+ var processBuilder = ExternalProcess .builder ()
176176 .command (this .nodeJSExec .getCanonicalPath (), nodeJSArgs )
177177 .copyOutputTo (stream );
178178 nodeJSEnvironment .forEach (processBuilder ::environment );
179179 process = processBuilder .start ();
180+ } catch (IOException e ) {
181+ throw new AppiumServerHasNotBeenStartedLocallyException (e );
182+ }
183+
184+ var didPingSucceed = false ;
185+ try {
180186 ping (startupTimeout );
181- } catch (Exception e ) {
182- final Optional <String > output = ofNullable (process )
183- .map (ExternalProcess ::getOutput )
184- .filter (o -> !isNullOrEmpty (o ));
185- destroyProcess ();
186- List <String > errorLines = new ArrayList <>();
187- errorLines .add ("The local appium server has not been started" );
188- errorLines .add (String .format ("Reason: %s" , e .getMessage ()));
189- if (e instanceof UrlChecker .TimeoutException ) {
190- errorLines .add (String .format (
191- "Consider increasing the server startup timeout value (currently %sms)" ,
192- startupTimeout .toMillis ()
193- ));
194- }
195- errorLines .add (
196- String .format ("Node.js executable path: %s" , nodeJSExec .getAbsolutePath ())
197- );
198- errorLines .add (String .format ("Arguments: %s" , nodeJSArgs ));
199- output .ifPresent (o -> errorLines .add (String .format ("Output: %s" , o )));
187+ didPingSucceed = true ;
188+ } catch (AppiumServerAvailabilityChecker .ConnectionTimeout
189+ | AppiumServerAvailabilityChecker .ConnectionError e ) {
190+ var errorLines = new ArrayList <>(generateDetailedErrorMessagePrefix (e ));
191+ errorLines .addAll (retrieveServerDebugInfo ());
200192 throw new AppiumServerHasNotBeenStartedLocallyException (
201193 String .join ("\n " , errorLines ), e
202194 );
195+ } catch (InterruptedException e ) {
196+ throw new RuntimeException (e );
197+ } finally {
198+ if (!didPingSucceed ) {
199+ destroyProcess ();
200+ }
203201 }
204202 } finally {
205203 lock .unlock ();
206204 }
207205 }
208206
207+ private List <String > generateDetailedErrorMessagePrefix (RuntimeException e ) {
208+ var errorLines = new ArrayList <String >();
209+ if (e instanceof AppiumServerAvailabilityChecker .ConnectionTimeout ) {
210+ errorLines .add (String .format (
211+ "Appium HTTP server is not listening at %s after %s ms timeout. "
212+ + "Consider increasing the server startup timeout value and "
213+ + "check the server log for possible error messages occurrences." , getUrl (),
214+ ((AppiumServerAvailabilityChecker .ConnectionTimeout ) e ).getTimeout ().toMillis ()
215+ ));
216+ } else if (e instanceof AppiumServerAvailabilityChecker .ConnectionError ) {
217+ var connectionError = (AppiumServerAvailabilityChecker .ConnectionError ) e ;
218+ var statusCode = connectionError .getResponseCode ();
219+ var statusUrl = connectionError .getStatusUrl ();
220+ var payload = connectionError .getPayload ();
221+ errorLines .add (String .format (
222+ "Appium HTTP server has started and is listening although we were "
223+ + "unable to get an OK response from %s. Make sure both the client "
224+ + "and the server use the same base path '%s' and check the server log for possible "
225+ + "error messages occurrences." , statusUrl , Optional .ofNullable (basePath ).orElse ("/" )
226+ ));
227+ errorLines .add (String .format ("Response status code: %s" , statusCode ));
228+ payload .ifPresent (p -> errorLines .add (String .format ("Response payload: %s" , p )));
229+ }
230+ return errorLines ;
231+ }
232+
233+ private List <String > retrieveServerDebugInfo () {
234+ var result = new ArrayList <String >();
235+ result .add (String .format ("Node.js executable path: %s" , nodeJSExec .getAbsolutePath ()));
236+ result .add (String .format ("Arguments: %s" , nodeJSArgs ));
237+ ofNullable (process )
238+ .map (ExternalProcess ::getOutput )
239+ .filter (o -> !isNullOrEmpty (o ))
240+ .ifPresent (o -> result .add (String .format ("Server log: %s" , o )));
241+ return result ;
242+ }
243+
209244 /**
210245 * Stops this service is it is currently running. This method will attempt to block until the
211246 * server has been fully shutdown.
0 commit comments