Skip to content

Commit

Permalink
Use HTTP API for readiness and tunnel information
Browse files Browse the repository at this point in the history
  • Loading branch information
alexh-sauce committed Oct 4, 2024
1 parent c7aeeea commit accc660
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -25,6 +29,7 @@
import org.json.JSONException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.json.JSONObject;

/**
* Provides common logic for the invocation of Sauce Connect v3 and v4 processes. The class
Expand All @@ -49,6 +54,7 @@ public abstract class AbstractSauceTunnelManager implements SauceTunnelManager {

private SauceREST sauceRest;
private SauceConnectEndpoint scEndpoint;
private SCMonitor scMonitor;

private AtomicInteger launchAttempts = new AtomicInteger(0);

Expand Down Expand Up @@ -114,6 +120,10 @@ public void setSauceRest(SauceREST sauceRest) {
this.scEndpoint = sauceRest.getSauceConnectEndpoint();
}

public void setSCMonitor(SCMonitor scMonitor) {
this.scMonitor = scMonitor;
}

/**
* Closes the Sauce Connect process
*
Expand Down Expand Up @@ -527,23 +537,28 @@ public Process openConnection(
try {
Semaphore semaphore = new Semaphore(1);
semaphore.acquire();
StreamGobbler errorGobbler = makeErrorGobbler(printStream, process.getErrorStream());
errorGobbler.start();
SystemOutGobbler outputGobbler =
makeOutputGobbler(printStream, process.getInputStream(), semaphore);
outputGobbler.start();

SCMonitor scMonitor;
if ( this.scMonitor != null ) {
scMonitor = this.scMonitor;
} else {
scMonitor = new SCMonitor("SCMonitor", port, LOGGER);
}

scMonitor.setSemaphore(semaphore);
scMonitor.start();

boolean sauceConnectStarted = semaphore.tryAcquire(3, TimeUnit.MINUTES);
if (sauceConnectStarted) {
if (outputGobbler.isFailed()) {
if (scMonitor.isFailed()) {
String message = "Error launching Sauce Connect";
logMessage(printStream, message);
// ensure that Sauce Connect process is closed
closeSauceConnectProcess(printStream, process);
throw new SauceConnectDidNotStartException(message);
} else {
// everything okay, continue the build
String provisionedTunnelId = outputGobbler.getTunnelId();
String provisionedTunnelId = scMonitor.getTunnelId();
if (provisionedTunnelId != null) {
tunnelInformation.setTunnelId(provisionedTunnelId);
waitForReadiness(provisionedTunnelId);
Expand Down Expand Up @@ -611,16 +626,6 @@ private void waitForReadiness(String tunnelId) {
}
}

public SystemErrorGobbler makeErrorGobbler(PrintStream printStream, InputStream errorStream) {
return new SystemErrorGobbler("ErrorGobbler", errorStream, printStream);
}

public SystemOutGobbler makeOutputGobbler(
PrintStream printStream, InputStream inputStream, Semaphore semaphore) {
return new SystemOutGobbler(
"OutputGobbler", inputStream, semaphore, printStream, getSauceStartedMessage());
}

private TunnelInformation getTunnelInformation(String name) {
if (name == null) {
return null;
Expand Down Expand Up @@ -682,11 +687,6 @@ public String getSauceConnectWorkingDirectory() {

public abstract File getSauceConnectLogFile(String options);

/**
* @return Text which indicates that Sauce Connect has started
*/
protected abstract String getSauceStartedMessage();

/** Base exception class which is thrown if an error occurs launching Sauce Connect. */
public static class SauceConnectException extends IOException {

Expand All @@ -710,49 +710,6 @@ public SauceConnectDidNotStartException(String message) {
}
}

/** Handles receiving and processing the output of an external process. */
protected abstract class StreamGobbler extends Thread {
private final PrintStream printStream;
private final InputStream is;

public StreamGobbler(String name, InputStream is, PrintStream printStream) {
super(name);
this.is = is;
this.printStream = printStream;
}

/** Opens a BufferedReader over the input stream, reads and processes each line. */
public void run() {
try {
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
processLine(line);
}
} catch (IOException ioe) {
// ignore stream closed errors
if (!(ioe.getMessage().equalsIgnoreCase("stream closed"))) {
ioe.printStackTrace();
}
}
}

/**
* Processes a line of output received by the stream gobbler.
*
* @param line line to process
*/
protected void processLine(String line) {
if (!quietMode) {
if (printStream != null) {
printStream.println(line);
}
LOGGER.info(line);
}
}
}

private int findFreePort() throws SauceConnectException {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
Expand All @@ -761,65 +718,102 @@ private int findFreePort() throws SauceConnectException {
}
}

/** Handles processing Sauce Connect output sent to stdout. */
public class SystemOutGobbler extends StreamGobbler {

private final Semaphore semaphore;
private final String startedMessage;
/** Monitors SC Process via HTTP API */
public class SCMonitor extends Thread {
private Semaphore semaphore;
private final int port;
private final Logger LOGGER;

private String tunnelId;
private boolean failed;
private boolean apiResponse;

private HttpClient client = HttpClient.newHttpClient();
private static final int sleepTime = 1000;

public SystemOutGobbler(
public SCMonitor(
String name,
InputStream is,
final Semaphore semaphore,
PrintStream printStream,
String startedMessage) {
super(name, is, printStream);
this.semaphore = semaphore;
this.startedMessage = startedMessage;
final int port,
final Logger logger) {
super(name);
this.port = port;
this.LOGGER = logger;
}

/**
* {@inheritDoc}
*
* <p>If the line contains the Sauce Connect started message, then release the semaphone, which
* will allow the build to resume.
*
* @param line Line being processed
*/
@Override
protected void processLine(String line) {
super.processLine(line);

System.out.println(line);
if (StringUtils.containsIgnoreCase(line, "sauce connect running id=")) {
tunnelId = StringUtils.substringAfter(line, "sauce connect running id=").trim();
}
if (StringUtils.containsIgnoreCase(line, "fatal error exiting")) {
failed = true;
}
if (StringUtils.containsIgnoreCase(line, startedMessage) || failed) {
// unlock processMonitor
semaphore.release();
}
public void setSemaphore(Semaphore semaphore) {
this.semaphore = semaphore;
}

public String getTunnelId() {
return tunnelId;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(String.format("http://localhost:%d/info", port)))
.GET()
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String responseBody = response.body();
JSONObject jsonObject = new JSONObject(responseBody);
if (jsonObject.has("tunnel_id")) {
this.LOGGER.info("Got tunnel ID", jsonObject.getString("tunnel_id"));
return jsonObject.getString("tunnel_id");
}
} catch (Exception e) {
this.LOGGER.info("Failed to get tunnel id", e);
return null;
}
this.LOGGER.info("Failed to get tunnel id");
return null;
}

public boolean isFailed() {
return failed;
}
}

/** Handles processing Sauce Connect output sent to stderr. */
public class SystemErrorGobbler extends StreamGobbler {
public void run() {
while (true) {
pollEndpoint();
if (this.semaphore.availablePermits() > 0) {
return;
}

try {
Thread.sleep(sleepTime);
} catch ( java.lang.InterruptedException e ) {
return;
}
}
}

private void pollEndpoint() {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(String.format("http://localhost:%d/status", port)))
.GET()
.build();

try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

this.LOGGER.info("Got http response", response.statusCode());

public SystemErrorGobbler(String name, InputStream is, PrintStream printStream) {
super(name, is, printStream);
if (response.statusCode() == 200) {
this.apiResponse = true;
String responseBody = response.body();
JSONObject jsonObject = new JSONObject(responseBody);
if (jsonObject.has("status") && "connected".equals(jsonObject.getString("status"))) {
this.LOGGER.info("Got connected status");
semaphore.release();
}
}
} catch ( Exception e ) {
if ( this.apiResponse ) {
// We've had a successful API endpoint read, but then it stopped responding, which means the process failed to start
this.failed = true;
this.LOGGER.warn("API stopped responding", e);
semaphore.release();
}
}

this.LOGGER.info("No API response yet");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,6 @@ public String getDefaultSauceConnectLogDirectory() {

private static final String WINDOWS_TEMP_DIR = System.getProperty("java.io.tmpdir");

/** Output from Sauce Connect process which indicates that it has been started. */
// TODO Replace with HTTP readiness check
private static final String SAUCE_CONNECT_STARTED =
"Sauce Connect is up, you may start your tests";

public static final String CURRENT_SC_VERSION = "5.1.3";
public static final LazyInitializer<String> LATEST_SC_VERSION = new Builder<LazyInitializer<String>, String>()
.setInitializer(SauceConnectManager::getLatestSauceConnectVersion)
Expand Down Expand Up @@ -389,12 +384,6 @@ private File getUnzipDir(File workingDirectory, OperatingSystem operatingSystem)
return new File(workingDirectory, operatingSystem.getDirectory(useLatestSauceConnect));
}

// TODO
/** {@inheritDoc} */
protected String getSauceStartedMessage() {
return SAUCE_CONNECT_STARTED;
}

protected boolean isConnected() {
HttpClient client = HttpClient.newHttpClient();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,4 @@ void testGetLogfile() {
AbstractSauceTunnelManager.getLogfile("-l first --logfile second -c -l third"),
"mix of -l and --logfile still returns the last one");
}

@Test
void testSystemOutGobbler_ProcessLine() {
Semaphore semaphore = new Semaphore(1);
SauceConnectManager man = new SauceConnectManager(true);
AbstractSauceTunnelManager.SystemOutGobbler sot = man.makeOutputGobbler(null, null, semaphore);
sot.processLine("sauce connect running id=tunnelId1");
assertEquals(sot.getTunnelId(), "tunnelId1");
sot.processLine("sauce connect running id=tunnelId2 ");
assertEquals(sot.getTunnelId(), "tunnelId2");
}
}
Loading

0 comments on commit accc660

Please sign in to comment.