diff --git a/pom.xml b/pom.xml index 3b67fa1d..e4545ad3 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.jenkins-ci.plugins plugin - 1.509 + 1.519 Parameterized-Remote-Trigger diff --git a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java index fa62789e..c20192e5 100644 --- a/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/ParameterizedRemoteTrigger/RemoteBuildConfiguration.java @@ -2,7 +2,6 @@ import hudson.AbortException; import hudson.FilePath; -import hudson.EnvVars; import hudson.Launcher; import hudson.Extension; import hudson.util.CopyOnWriteList; @@ -217,8 +216,7 @@ private List getCleanedParameters() { * Same as "getParameterList", but removes comments and empty strings Notice that no type of character encoding is * happening at this step. All encoding happens in the "buildUrlQueryString" method. * - * @param List - * parameters + * @param parameters * @return List of build parameters */ private List getCleanedParameters(List parameters) { @@ -500,7 +498,7 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListener lis String preCheckUrlString = this.buildGetUrl(jobName, securityToken); preCheckUrlString += "/lastBuild"; preCheckUrlString += "/api/json/"; - JSONObject preCheckResponse = sendHTTPCall(preCheckUrlString, "GET", build, listener); + JSONObject preCheckResponse = sendHTTPCallAndGetJSON(preCheckUrlString, "GET", build, listener); if ( preCheckResponse != null ) { // check the latest build on the remote server to see if it's running - if so wait until it has stopped. @@ -508,7 +506,7 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListener lis // if result is null the build hasn't finished - but might not have started running. while (preCheckResponse.getBoolean("building") == true || preCheckResponse.getString("result") == null) { listener.getLogger().println("Remote build is currently running - waiting for it to finish."); - preCheckResponse = sendHTTPCall(preCheckUrlString, "POST", build, listener); + preCheckResponse = sendHTTPCallAndGetJSON(preCheckUrlString, "POST", build, listener); listener.getLogger().println("Waiting for " + this.pollInterval + " seconds until next retry."); // Sleep for 'pollInterval' seconds. @@ -531,15 +529,6 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListener lis String queryUrlString = this.buildGetUrl(jobName, securityToken); queryUrlString += "/api/json/"; - //listener.getLogger().println("Getting ID of next job to build. URL: " + queryUrlString); - JSONObject queryResponseObject = sendHTTPCall(queryUrlString, "GET", build, listener); - if (queryResponseObject == null ) { - //This should not happen as this page should return a JSON object - this.failBuild(new Exception("Got a blank response from Remote Jenkins Server [" + remoteServerURL + "], cannot continue."), listener); - } - - int nextBuildNumber = queryResponseObject.getInt("nextBuildNumber"); - if (this.getOverrideAuth()) { listener.getLogger().println( "Using job-level defined credentails in place of those from remote Jenkins config [" @@ -547,43 +536,47 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListener lis } listener.getLogger().println("Triggering remote job now."); - sendHTTPCall(triggerUrlString, "POST", build, listener); - // Validate the build number via parameters - foundIt: for (int tries = 3; tries > 0; tries--) { - for (int buildNumber : new SearchPattern(nextBuildNumber, 2)) { - listener.getLogger().println("Checking parameters of #" + buildNumber); - String validateUrlString = this.buildGetUrl(jobName, securityToken) + "/" + buildNumber + "/api/json/"; - JSONObject validateResponse = sendHTTPCall(validateUrlString, "GET", build, listener); - if (validateResponse == null) { - listener.getLogger().println("Query failed."); - continue; - } - JSONArray actions = validateResponse.getJSONArray("actions"); - for (int i = 0; i < actions.size(); i++) { - JSONObject action = actions.getJSONObject(i); - if (!action.has("parameters")) continue; - JSONArray parameters = action.getJSONArray("parameters"); - // Check if the parameters match - if (compareParameters(listener, parameters, cleanedParams)) { - // We now have a very high degree of confidence that this is the correct build. - // It is still possible that this is a false positive if there are no parameters, - // or multiple jobs use the same parameters. - nextBuildNumber = buildNumber; - break foundIt; - } - // This is the wrong build - break; + String location = sendHTTPCallAndGetLocation(triggerUrlString, "POST", build, listener); + + listener.getLogger().println("Job scheduled in location: " + location); + + int nextBuildNumber = 0; + + + do + { + JSONObject responseObj = sendHTTPCallAndGetJSON(location + "/api/json", "GET", build, listener); + + JSONObject executable = responseObj.getJSONObject("executable"); + + if ((executable != null) && (!executable.isNullObject()) && (executable.get("number") != null)) { + nextBuildNumber = executable.getInt("number"); + break; + } else + { + //Don't sleep longer than a minute for this step, as the queue item is only valid for 5 minutes. + //See https://issues.jenkins-ci.org/browse/JENKINS-26228 + listener.getLogger().println("Waiting for remote build to start."); + if(this.pollInterval > 60) + { + listener.getLogger().println("Waiting for 60 seconds before polling again, unfortunately this step needs to be more frequent than you requested. See https://issues.jenkins-ci.org/browse/JENKINS-26228 as to why."); + } else + { + listener.getLogger().println("Waiting for " + this.pollInterval + " seconds until next poll."); } - // Sleep for 'pollInterval' seconds. - // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) try { - Thread.sleep(this.pollInterval * 1000); - } catch (InterruptedException e) { + Thread.sleep(Math.min(this.pollInterval, 60) * 1000); + } catch(InterruptedException e) + { this.failBuild(e, listener); } } - } + + } while(true); + + + listener.getLogger().println("This job is build #[" + Integer.toString(nextBuildNumber) + "] on the remote server."); BuildInfoExporterAction.addBuildInfoExporterAction(build, jobName, nextBuildNumber, Result.NOT_BUILT); @@ -722,7 +715,7 @@ public String getBuildStatus(String buildUrlString, AbstractBuild build, BuildLi + this.getRemoteJenkinsName() + "]"); } - JSONObject responseObject = sendHTTPCall(buildUrlString, "GET", build, listener); + JSONObject responseObject = sendHTTPCallAndGetJSON(buildUrlString, "GET", build, listener); // get the next build from the location @@ -761,7 +754,7 @@ public String getBuildUrl(String buildUrlString, AbstractBuild build, BuildListe + this.getRemoteJenkinsName() + "]"); } - JSONObject responseObject = sendHTTPCall(buildUrlString, "GET", build, listener); + JSONObject responseObject = sendHTTPCallAndGetJSON(buildUrlString, "GET", build, listener); // get the next build from the location @@ -779,24 +772,7 @@ public String getBuildUrl(String buildUrlString, AbstractBuild build, BuildListe public String getConsoleOutput(String urlString, String requestType, AbstractBuild build, BuildListener listener) throws IOException { - return getConsoleOutput( urlString, requestType, build, listener, 1 ); - } - - /** - * Orchestrates all calls to the remote server. - * Also takes care of any credentials or failed-connection retries. - * - * @param urlString the URL that needs to be called - * @param requestType the type of request (GET, POST, etc) - * @param build the build that is being triggered - * @param listener build listener - * @return a valid JSON object, or null - * @throws IOException - */ - public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBuild build, BuildListener listener) - throws IOException { - - return sendHTTPCall( urlString, requestType, build, listener, 1 ); + return getConsoleOutput(urlString, requestType, build, listener, 1); } public String getConsoleOutput(String urlString, String requestType, AbstractBuild build, BuildListener listener, int numberOfAttempts) @@ -869,7 +845,7 @@ public String getConsoleOutput(String urlString, String requestType, AbstractBui consoleOutput = response.toString(); } catch (IOException e) { - + listener.getLogger().println(e.getMessage()); //If we have connectionRetryLimit set to > 0 then retry that many times. if( numberOfAttempts <= retryLimit) { listener.getLogger().println("Connection to remote server failed, waiting for to retry - " + this.pollInterval + " seconds until next attempt."); @@ -911,17 +887,88 @@ public String getConsoleOutput(String urlString, String requestType, AbstractBui } /** - * Same as sendHTTPCall, but keeps track of the number of failed connection attempts (aka: the number of times this - * method has been called). + * Sends a request to the server and retrieves the HTTP Field Location response + * This is primarily useful to determine what build we queued + * + * @return HTTP Header Location Value + */ + public String sendHTTPCallAndGetLocation(String urlString, String requestType, AbstractBuild build, final BuildListener listener) + throws IOException { + ProcessHTTPResponse processHTTPResponse = new ProcessHTTPResponse() { + public String getResponse(HttpURLConnection connection) throws IOException { + String location = connection.getHeaderField("Location"); + if(location == null) + { + throw new IOException("No location retrieved from server"); + } else + { + return location; + } + } + }; + return sendHTTPCallAndProcessResponse(urlString,requestType,build,listener,1,60, processHTTPResponse); + } + + /** + * * In the case of a failed connection, the method calls it self recursively and increments numberOfAttempts - * - * @see sendHTTPCall - * @param numberOfAttempts number of time that the connection has been attempted - * @return + * + * @return JSONObject * @throws IOException */ - public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBuild build, BuildListener listener, int numberOfAttempts) + public JSONObject sendHTTPCallAndGetJSON(String urlString, String requestType, AbstractBuild build, final BuildListener listener) throws IOException { + ProcessHTTPResponse processJSONObject = new ProcessHTTPResponse() { + public JSONObject getResponse(HttpURLConnection connection) throws IOException { + InputStream is; + try { + is = connection.getInputStream(); + } catch (FileNotFoundException e) { + // In case of a e.g. 404 status + is = connection.getErrorStream(); + } + + BufferedReader rd = new BufferedReader(new InputStreamReader(is)); + String line; + // String response = ""; + StringBuilder response = new StringBuilder(); + + while ((line = rd.readLine()) != null) { + response.append(line); + } + rd.close(); + + // JSONSerializer serializer = new JSONSerializer(); + // need to parse the data we get back into struct + //listener.getLogger().println("Called URL: '" + urlString + "', got response: '" + response.toString() + "'"); + + //Solving issue reported in this comment: https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/3#issuecomment-39369194 + //Seems like in Jenkins version 1.547, when using "/build" (job API for non-parameterized jobs), it returns a string indicating the status. + //But in newer versions of Jenkins, it just returns an empty response. + //So we need to compensate and check for both. + if (JSONUtils.mayBeJSON(response.toString()) == false) { + listener.getLogger().println("Remote Jenkins server returned empty response or invalid JSON - but we can still proceed with the remote build."); + return null; + } else { + return (JSONObject) JSONSerializer.toJSON(response.toString()); + } + + } + }; + + return sendHTTPCallAndProcessResponse(urlString, requestType, build, listener, 1, this.pollInterval, processJSONObject); + } + + /** + * Process the HTTP call and then uses the supplied delegate (or strategy) to determine what we want from the HTTP call. + * + * @param + * @return + * @throws IOException + */ + private T sendHTTPCallAndProcessResponse(String urlString, String requestType, AbstractBuild build, BuildListener listener, int numberOfAttempts, int pollInterval, ProcessHTTPResponse httpProcessor) throws IOException + { + RemoteJenkinsServer remoteServer = this.findRemoteHost(this.getRemoteJenkinsName()); int retryLimit = this.getConnectionRetryLimit(); @@ -932,7 +979,7 @@ public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBui HttpURLConnection connection = null; - JSONObject responseObject = null; + URL buildUrl = new URL(urlString); connection = (HttpURLConnection) buildUrl.openConnection(); @@ -968,52 +1015,30 @@ public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBui // wait up to 5 seconds for the connection to be open connection.setConnectTimeout(5000); connection.connect(); - - InputStream is; - try { - is = connection.getInputStream(); - } catch (FileNotFoundException e) { - // In case of a e.g. 404 status - is = connection.getErrorStream(); - } - - BufferedReader rd = new BufferedReader(new InputStreamReader(is)); - String line; - // String response = ""; - StringBuilder response = new StringBuilder(); - - while ((line = rd.readLine()) != null) { - response.append(line); - } - rd.close(); - - // JSONSerializer serializer = new JSONSerializer(); - // need to parse the data we get back into struct - //listener.getLogger().println("Called URL: '" + urlString + "', got response: '" + response.toString() + "'"); - - //Solving issue reported in this comment: https://github.com/jenkinsci/parameterized-remote-trigger-plugin/pull/3#issuecomment-39369194 - //Seems like in Jenkins version 1.547, when using "/build" (job API for non-parameterized jobs), it returns a string indicating the status. - //But in newer versions of Jenkins, it just returns an empty response. - //So we need to compensate and check for both. - if ( JSONUtils.mayBeJSON(response.toString()) == false) { - listener.getLogger().println("Remote Jenkins server returned empty response or invalid JSON - but we can still proceed with the remote build."); - return null; - } else { - responseObject = (JSONObject) JSONSerializer.toJSON(response.toString()); + + int responseCode = connection.getResponseCode(); + + if(responseCode == 404) + { + // I am too lazy to figure out why 404 errors don't get displayed to the user properly, all other errors do. + // I did try and remove the catching of the FileNotFoundException but that still didn't make it clear. + listener.getLogger().println("Recieved HTTP Message 404 from server. Please check that the job or build still exists. Request:" + connection.getURL().toString() ); } + return httpProcessor.getResponse(connection); + } catch (IOException e) { listener.getLogger().println(e.getMessage()); //If we have connectionRetryLimit set to > 0 then retry that many times. if( numberOfAttempts <= retryLimit) { - listener.getLogger().println("Connection to remote server failed, waiting for to retry - " + this.pollInterval + " seconds until next attempt."); + listener.getLogger().println("Connection to remote server failed, waiting for to retry - " + pollInterval + " seconds until next attempt."); e.printStackTrace(); // Sleep for 'pollInterval' seconds. // Sleep takes miliseconds so need to convert this.pollInterval to milisecopnds (x 1000) try { // Could do with a better way of sleeping... - Thread.sleep(this.pollInterval * 1000); + Thread.sleep(pollInterval * 1000); } catch (InterruptedException ex) { this.failBuild(ex, listener); } @@ -1021,7 +1046,7 @@ public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBui listener.getLogger().println("Retry attempt #" + numberOfAttempts + " out of " + retryLimit ); numberOfAttempts++; - responseObject = sendHTTPCall(urlString, requestType, build, listener, numberOfAttempts); + return sendHTTPCallAndProcessResponse(urlString, requestType, build, listener, numberOfAttempts, pollInterval, httpProcessor); }else if(numberOfAttempts > retryLimit){ //reached the maximum number of retries, time to fail this.failBuild(new Exception("Max number of connection retries have been exeeded."), listener); @@ -1041,7 +1066,7 @@ public JSONObject sendHTTPCall(String urlString, String requestType, AbstractBui // this.listener = null; } - return responseObject; + return null; } /** @@ -1162,7 +1187,7 @@ private boolean isRemoteJobParameterized(String jobName, AbstractBuild build, Bu remoteServerUrl += "/api/json"; try { - JSONObject response = sendHTTPCall(remoteServerUrl, "GET", build, listener); + JSONObject response = sendHTTPCallAndGetJSON(remoteServerUrl, "GET", build, listener); if(response.getJSONArray("actions").size() >= 1){ isParameterized = true; @@ -1289,4 +1314,14 @@ public void setRemoteSites(RemoteJenkinsServer... remoteSites) { this.remoteSites.replaceBy(remoteSites); } } + + private interface ProcessHTTPResponse{ + /** + * Retrieves the object T + * @param connection + * @throws IOException if there is a problem + * @return + */ + public T getResponse(HttpURLConnection connection) throws IOException; + } }