diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 1b35752..f6e8f14 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -53,11 +53,11 @@ jobs: uses: actions/checkout@v2.4.0 with: fetch-depth: 0 - - name: Set up JDK 8 + - name: Set up JDK 17 uses: actions/setup-java@v2.5.0 with: distribution: temurin - java-version: 8 + java-version: 17 - name: Check Secrets env: DEBUG_USERNAME: ${{ secrets.MAVEN_USERNAME }} diff --git a/Jenkinsfile b/Jenkinsfile index 48bf230..ae2fc83 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1 +1,6 @@ -buildPlugin() \ No newline at end of file +buildPlugin( + useContainerAgent: true, // Set to `false` if you need to use Docker for containerized tests + configurations: [ + [platform: 'linux', jdk: 17], + ] +) \ No newline at end of file diff --git a/README.md b/README.md index 57ee58f..16f41eb 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,16 @@ To learn more about using this plugin, you can refer to the following documents: ## Development +Use `mvn` version from `3.8.1`. + Use the command `mvn clean hpi:run -Djetty.port=8090` to debug the plugin. See the instructions at https://wiki.jenkins.io/display/JENKINS/Plugin+tutorial. +### Requisition + +Jenkins: 2.448 + ## Companion products ### Katalon TestOps diff --git a/pom.xml b/pom.xml index 458e615..ac59230 100644 --- a/pom.xml +++ b/pom.xml @@ -5,31 +5,28 @@ org.jenkins-ci.plugins plugin - 3.56 + 4.79 org.jenkins-ci.plugins katalon - 1.0.34 + 1.0.38 hpi Katalon Plugin - Execute Katalon Studio in Jenkins - https://github.com/jenkinsci/katalon-plugin + https://github.com/katalon-studio/katalon-studio-jenkins-plugin - 1.625.3 - 8 + 2.448 + 17 1.20 - 2.13.13 - 3.12.0 - 1.0.17 + 1.0.18 4.5.13 @@ -49,9 +46,9 @@ - devalex88 - Alex - dev.alex.88@gmail.com + thvu-katalon + Vu + vu.than@katalon.com @@ -60,7 +57,7 @@ scm:git:git@github.com:jenkinsci/${project.artifactId}-plugin.git https://github.com/jenkinsci/${project.artifactId}-plugin HEAD - + diff --git a/src/main/java/com/katalon/jenkins/plugin/ExecuteKatalonStudioTask.java b/src/main/java/com/katalon/jenkins/plugin/ExecuteKatalonStudioTask.java index 4bc597d..9a075c7 100644 --- a/src/main/java/com/katalon/jenkins/plugin/ExecuteKatalonStudioTask.java +++ b/src/main/java/com/katalon/jenkins/plugin/ExecuteKatalonStudioTask.java @@ -85,7 +85,14 @@ public void setXvfbConfiguration(String xvfbConfiguration) { @Override public boolean perform(AbstractBuild abstractBuild, Launcher launcher, BuildListener buildListener) - throws InterruptedException, IOException { + throws InterruptedException, IOException { + + // Check for interruption before starting + if (isInterrupted()) { + buildListener.getLogger().println("Build was cancelled before Katalon execution started"); + throw new InterruptedException("Build was cancelled"); + } + FilePath workspace = abstractBuild.getWorkspace(); EnvVars buildEnvironment = abstractBuild.getEnvironment(buildListener); return doPerform(workspace, buildEnvironment, launcher, buildListener); @@ -94,24 +101,37 @@ public boolean perform(AbstractBuild abstractBuild, Launcher launcher, Bui @Override public void perform(@Nonnull Run run, @Nonnull FilePath filePath, @Nonnull Launcher launcher, @Nonnull TaskListener taskListener) - throws InterruptedException, IOException { + throws InterruptedException, IOException { + + // Check for interruption before starting + if (isInterrupted()) { + taskListener.getLogger().println("Build was cancelled before Katalon execution started"); + throw new InterruptedException("Build was cancelled"); + } + EnvVars buildEnvironment = run.getEnvironment(taskListener); doPerform(filePath, buildEnvironment, launcher, taskListener); } private boolean doPerform(FilePath workspace, EnvVars buildEnvironment, Launcher launcher, TaskListener taskListener) - throws IOException, InterruptedException { - return ExecuteKatalonStudioHelper.executeKatalon( - workspace, - buildEnvironment, - launcher, - taskListener, - version, - location, - executeArgs, - x11Display, - xvfbConfiguration); + throws IOException, InterruptedException { + + try { + return ExecuteKatalonStudioHelper.executeKatalon( + workspace, + buildEnvironment, + launcher, + taskListener, + version, + location, + executeArgs, + x11Display, + xvfbConfiguration); + } catch (InterruptedException e) { + taskListener.getLogger().println("Katalon execution was interrupted due to build cancellation"); + throw e; // Re-throw to ensure proper build status + } } @Symbol("executeKatalon") @@ -133,4 +153,8 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc return super.configure(req, formData); } } + + private static boolean isInterrupted() { + return Thread.currentThread().isInterrupted(); + } } \ No newline at end of file diff --git a/src/main/java/com/katalon/jenkins/plugin/helper/ExecuteKatalonStudioHelper.java b/src/main/java/com/katalon/jenkins/plugin/helper/ExecuteKatalonStudioHelper.java index af5ea76..a8f121e 100644 --- a/src/main/java/com/katalon/jenkins/plugin/helper/ExecuteKatalonStudioHelper.java +++ b/src/main/java/com/katalon/jenkins/plugin/helper/ExecuteKatalonStudioHelper.java @@ -7,10 +7,12 @@ import hudson.FilePath; import hudson.Launcher; import hudson.model.TaskListener; +import hudson.remoting.VirtualChannel; import jenkins.security.MasterToSlaveCallable; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; public class ExecuteKatalonStudioHelper { @@ -23,41 +25,188 @@ public static boolean executeKatalon( String location, String executeArgs, String x11Display, - String xvfbConfiguration) { + String xvfbConfiguration) throws InterruptedException { Logger logger = new JenkinsLogger(taskListener); try { - return launcher.getChannel().call(new MasterToSlaveCallable() { - @Override - public Boolean call() throws Exception { - - Logger logger = new JenkinsLogger(taskListener); - - if (workspace != null) { - String workspaceLocation = workspace.getRemote(); - - if (workspaceLocation != null) { - Map environmentVariables = new HashMap<>(); - environmentVariables.putAll(System.getenv()); - buildEnvironment.entrySet() - .forEach(entry -> environmentVariables.put(entry.getKey(), entry.getValue())); - return KatalonUtils.executeKatalon( - logger, - version, - location, - workspaceLocation, - executeArgs, - x11Display, - xvfbConfiguration, - environmentVariables); - } - } - return true; - } - }); + VirtualChannel channel = launcher.getChannel(); + if (channel == null) { + throw new Exception("Channel not found!"); + } + return channel.call(new InterruptibleKatalonCallable( + taskListener, workspace, buildEnvironment, logger, version, + location, executeArgs, x11Display, xvfbConfiguration)); + } catch (InterruptedException e) { + logger.info("Katalon execution was interrupted"); + throw e; // Re-throw InterruptedException to maintain cancellation behavior } catch (Exception e) { String stackTrace = Throwables.getStackTraceAsString(e); logger.info(stackTrace); return false; } } + + private static class InterruptibleKatalonCallable extends MasterToSlaveCallable { + private final TaskListener taskListener; + private final FilePath workspace; + private final EnvVars buildEnvironment; + private final Logger logger; + private final String version; + private final String location; + private final String executeArgs; + private final String x11Display; + private final String xvfbConfiguration; + private final AtomicBoolean cancelled = new AtomicBoolean(false); + + public InterruptibleKatalonCallable(TaskListener taskListener, FilePath workspace, + EnvVars buildEnvironment, Logger logger, String version, String location, + String executeArgs, String x11Display, String xvfbConfiguration) { + this.taskListener = taskListener; + this.workspace = workspace; + this.buildEnvironment = buildEnvironment; + this.logger = logger; + this.version = version; + this.location = location; + this.executeArgs = executeArgs; + this.x11Display = x11Display; + this.xvfbConfiguration = xvfbConfiguration; + } + + @Override + public Boolean call() throws Exception { + Logger logger = new JenkinsLogger(taskListener); + + // Check for interruption at the start + if (Thread.currentThread().isInterrupted()) { + logger.info("Thread was interrupted before Katalon execution started"); + throw new InterruptedException("Execution was cancelled"); + } + + if (workspace != null) { + String workspaceLocation = workspace.getRemote(); + + if (workspaceLocation != null) { + Map environmentVariables = new HashMap<>(); + environmentVariables.putAll(System.getenv()); + buildEnvironment.entrySet() + .forEach(entry -> environmentVariables.put(entry.getKey(), entry.getValue())); + + // Check for interruption before starting Katalon + if (Thread.currentThread().isInterrupted()) { + logger.info("Thread was interrupted before calling KatalonUtils.executeKatalon"); + throw new InterruptedException("Execution was cancelled"); + } + try { + // Create a wrapper that can be interrupted + return executeKatalonWithInterruption( + logger, version, location, workspaceLocation, + executeArgs, x11Display, xvfbConfiguration, environmentVariables); + } catch (InterruptedException e) { + logger.info("Katalon execution was interrupted due to build cancellation"); + cancelled.set(true); + throw e; + } + } + } + return true; + } + + private Boolean executeKatalonWithInterruption( + Logger logger, String version, String location, String workspaceLocation, + String executeArgs, String x11Display, String xvfbConfiguration, + Map environmentVariables) throws Exception { + + // Create a thread to run Katalon execution + final Exception[] executionException = new Exception[1]; + final Boolean[] result = new Boolean[1]; + + Thread katalonThread = new Thread(() -> { + try { + result[0] = KatalonUtils.executeKatalon( + logger, version, location, workspaceLocation, + executeArgs, x11Display, xvfbConfiguration, environmentVariables); + } catch (Exception e) { + executionException[0] = e; + } + }); + + katalonThread.start(); + + // Monitor for interruption while Katalon is running + while (katalonThread.isAlive()) { + if (Thread.currentThread().isInterrupted()) { + logger.info("Build cancellation detected, force stopping Katalon execution"); + + // Force kill Katalon processes + forceStopKatalonProcesses(logger); + + // Interrupt the Katalon thread + katalonThread.interrupt(); + + // Wait a bit for graceful shutdown + try { + katalonThread.join(3000); // Wait up to 3 seconds + } catch (InterruptedException ie) { + // If we're interrupted while waiting, force stop + Thread.currentThread().interrupt(); + } + throw new InterruptedException("Katalon execution was forcibly cancelled"); + } + + try { + Thread.sleep(500); // Check every 0.5 seconds for faster response + } catch (InterruptedException e) { + // If interrupted while sleeping, force stop Katalon and exit + forceStopKatalonProcesses(logger); + katalonThread.interrupt(); + throw e; + } + } + // Check if there was an exception during execution + if (executionException[0] != null) { + throw executionException[0]; + } + return result[0] != null ? result[0] : false; + } + + private void forceStopKatalonProcesses(Logger logger) { + try { + String os = System.getProperty("os.name").toLowerCase(); + + if (os.contains("win")) { + // Windows - kill Katalon processes + executeCommand(new String[] { "taskkill", "/F", "/IM", "katalonc.exe" }, logger); + } else { + // Linux/Unix - kill Katalon processes + executeCommand(new String[] { "pkill", "-f", "katalonc" }, logger); + // Force kill if regular kill doesn't work + executeCommand(new String[] { "pkill", "-9", "-f", "katalonc" }, logger); + } + } catch (Exception e) { + logger.info("Error while trying to force stop Katalon processes: " + e.getMessage()); + } + } + + private void executeCommand(String[] command, Logger logger) { + try { + ProcessBuilder pb = new ProcessBuilder(command); + pb.redirectErrorStream(true); + Process process = pb.start(); + + // Wait for command to complete (with timeout) + boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS); + + if (!finished) { + logger.info("Force stop command timed out: " + String.join(" ", command)); + process.destroyForcibly(); + } else { + int exitCode = process.exitValue(); + logger.info("Force stop command completed with exit code " + exitCode + ": " + + String.join(" ", command)); + } + } catch (Exception e) { + logger.info( + "Failed to execute force stop command: " + String.join(" ", command) + " - " + e.getMessage()); + } + } + } } diff --git a/src/main/resources/index.jelly b/src/main/resources/index.jelly new file mode 100644 index 0000000..679377c --- /dev/null +++ b/src/main/resources/index.jelly @@ -0,0 +1,4 @@ + +
+ Execute Katalon Studio in Jenkins +
\ No newline at end of file