17
17
package io .appium .java_client .service .local ;
18
18
19
19
import com .google .common .annotations .VisibleForTesting ;
20
+ import lombok .Getter ;
20
21
import lombok .SneakyThrows ;
21
- import org .openqa .selenium .net .UrlChecker ;
22
22
import org .openqa .selenium .os .ExternalProcess ;
23
23
import org .openqa .selenium .remote .service .DriverService ;
24
24
import org .slf4j .Logger ;
30
30
import java .io .File ;
31
31
import java .io .IOException ;
32
32
import java .io .OutputStream ;
33
- import java .net .MalformedURLException ;
34
33
import java .net .URL ;
35
34
import java .time .Duration ;
36
35
import java .util .ArrayList ;
37
36
import java .util .List ;
38
37
import java .util .Map ;
39
38
import java .util .Optional ;
40
- import java .util .concurrent .TimeUnit ;
41
39
import java .util .concurrent .locks .ReentrantLock ;
42
40
import java .util .function .BiConsumer ;
43
41
import java .util .function .Consumer ;
@@ -67,7 +65,9 @@ public final class AppiumDriverLocalService extends DriverService {
67
65
private final Duration startupTimeout ;
68
66
private final ReentrantLock lock = new ReentrantLock (true ); //uses "fair" thread ordering policy
69
67
private final ListOutputStream stream = new ListOutputStream ().add (System .out );
68
+ private final AppiumServerAvailabilityChecker availabilityChecker = new AppiumServerAvailabilityChecker ();
70
69
private final URL url ;
70
+ @ Getter
71
71
private String basePath ;
72
72
73
73
private ExternalProcess process = null ;
@@ -97,10 +97,6 @@ public AppiumDriverLocalService withBasePath(String basePath) {
97
97
return this ;
98
98
}
99
99
100
- public String getBasePath () {
101
- return this .basePath ;
102
- }
103
-
104
100
@ SneakyThrows
105
101
private static URL addSuffix (URL url , String suffix ) {
106
102
return url .toURI ().resolve ("." + (suffix .startsWith ("/" ) ? suffix : "/" + suffix )).toURL ();
@@ -131,36 +127,40 @@ public boolean isRunning() {
131
127
}
132
128
133
129
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 ) {
137
133
return false ;
138
- } catch (MalformedURLException e ) {
139
- throw new AppiumServerHasNotBeenStartedLocallyException ( e . getMessage (), e );
134
+ } catch (InterruptedException e ) {
135
+ throw new RuntimeException ( e );
140
136
}
141
137
} finally {
142
138
lock .unlock ();
143
139
}
140
+ }
144
141
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 );
145
146
}
146
147
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 ();
150
150
// The operating system will block direct access to the universal broadcast IP address
151
151
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" );
155
156
}
156
- URL status = addSuffix (baseURL , "/status" );
157
- new UrlChecker ().waitUntilAvailable (timeout .toMillis (), TimeUnit .MILLISECONDS , status );
157
+ return url ;
158
158
}
159
159
160
160
/**
161
161
* Starts the defined appium server.
162
162
*
163
- * @throws AppiumServerHasNotBeenStartedLocallyException If an error occurs while spawning the child process .
163
+ * @throws AppiumServerHasNotBeenStartedLocallyException If an error occurs on Appium server startup .
164
164
* @see #stop()
165
165
*/
166
166
@ Override
@@ -172,40 +172,75 @@ public void start() throws AppiumServerHasNotBeenStartedLocallyException {
172
172
}
173
173
174
174
try {
175
- ExternalProcess . Builder processBuilder = ExternalProcess .builder ()
175
+ var processBuilder = ExternalProcess .builder ()
176
176
.command (this .nodeJSExec .getCanonicalPath (), nodeJSArgs )
177
177
.copyOutputTo (stream );
178
178
nodeJSEnvironment .forEach (processBuilder ::environment );
179
179
process = processBuilder .start ();
180
+ } catch (IOException e ) {
181
+ throw new AppiumServerHasNotBeenStartedLocallyException (e );
182
+ }
183
+
184
+ var didPingSucceed = false ;
185
+ try {
180
186
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 ());
200
192
throw new AppiumServerHasNotBeenStartedLocallyException (
201
193
String .join ("\n " , errorLines ), e
202
194
);
195
+ } catch (InterruptedException e ) {
196
+ throw new RuntimeException (e );
197
+ } finally {
198
+ if (!didPingSucceed ) {
199
+ destroyProcess ();
200
+ }
203
201
}
204
202
} finally {
205
203
lock .unlock ();
206
204
}
207
205
}
208
206
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
+
209
244
/**
210
245
* Stops this service is it is currently running. This method will attempt to block until the
211
246
* server has been fully shutdown.
0 commit comments