├── .github ├── CODEOWNERS ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── cd.yaml │ └── jenkins-security-scan.yml ├── .gitignore ├── .mvn ├── extensions.xml └── maven.config ├── CHANGELOG.md ├── Jenkinsfile ├── README.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── jenkinsci │ │ └── plugins │ │ └── durabletask │ │ ├── AgentInfo.java │ │ ├── BourneShellScript.java │ │ ├── ContinuedTask.java │ │ ├── Controller.java │ │ ├── DurableTask.java │ │ ├── DurableTaskDescriptor.java │ │ ├── FileMonitoringTask.java │ │ ├── Handler.java │ │ ├── PowershellScript.java │ │ ├── WindowsBatchScript.java │ │ └── executors │ │ ├── ContinuableExecutable.java │ │ ├── ContinuedTask.java │ │ └── OnceRetentionStrategy.java └── resources │ ├── index.jelly │ └── org │ └── jenkinsci │ └── plugins │ └── durabletask │ ├── BourneShellScript │ ├── config.jelly │ └── help-script.html │ ├── Messages.properties │ ├── PowershellScript │ ├── config.jelly │ └── help-script.html │ ├── WindowsBatchScript │ ├── config.jelly │ └── help-script.html │ ├── executors │ └── Messages.properties │ └── powershellHelper.ps1 └── test ├── java └── org │ └── jenkinsci │ └── plugins │ └── durabletask │ ├── AlpineFixture.java │ ├── BourneShellScriptTest.java │ ├── CentOSFixture.java │ ├── EncodingTest.java │ ├── PowerShellCoreFixture.java │ ├── PowerShellCoreScriptTest.java │ ├── PowershellScriptTest.java │ ├── SlimFixture.java │ ├── WindowsBatchScriptTest.java │ └── executors │ ├── ContinuedTaskTest.java │ ├── MockTask.java │ └── OnceRetentionStrategyTest.java └── resources └── org └── jenkinsci └── plugins └── durabletask ├── AlpineFixture └── Dockerfile ├── CentOSFixture └── Dockerfile ├── PowerShellCoreFixture └── Dockerfile └── SlimFixture └── Dockerfile /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jenkinsci/durable-task-plugin-developers 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "maven" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.github/workflows/cd.yaml: -------------------------------------------------------------------------------- 1 | # Note: additional setup is required, see https://www.jenkins.io/redirect/continuous-delivery-of-plugins 2 | 3 | name: cd 4 | on: 5 | workflow_dispatch: 6 | check_run: 7 | types: 8 | - completed 9 | 10 | jobs: 11 | maven-cd: 12 | uses: jenkins-infra/github-reusable-workflows/.github/workflows/maven-cd.yml@v1 13 | secrets: 14 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 15 | MAVEN_TOKEN: ${{ secrets.MAVEN_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/jenkins-security-scan.yml: -------------------------------------------------------------------------------- 1 | name: Jenkins Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [ opened, synchronize, reopened ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | security-events: write 13 | contents: read 14 | actions: read 15 | 16 | jobs: 17 | security-scan: 18 | uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 19 | with: 20 | java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. 21 | # java-version: 21 # Optionally specify what version of Java to set up for the build, or remove to use a recent default. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | work 3 | .idea 4 | *.iws 5 | *.iml 6 | *.ipr 7 | .settings 8 | .project 9 | .classpath 10 | durable_task_monitor_* 11 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.jenkins.tools.incrementals 4 | git-changelist-maven-extension 5 | 1.8 6 | 7 | 8 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Pconsume-incrementals 2 | -Pmight-produce-incrementals 3 | -Dchangelist.format=%d.v%s 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | * For newer versions, see [GitHub Releases](https://github.com/jenkinsci/durable-task-plugin/releases) 4 | 5 | ### Version 1.36 6 | 7 | Release date: 2021-05-04 8 | 9 | - Fix: PowerShell command invocation errors will now fail at the pipeline step ([JENKINS-59529](https://issues.jenkins.io/browse/JENKINS-59529)) 10 | - Add an option to load the PowerShell profile ([PR 130](https://github.com/jenkinsci/durable-task-plugin/pull/130)) 11 | 12 | ### Version 1.35 13 | 14 | Release date: 2020-09-02 15 | 16 | - Fix: Convert `FileMonitoringTask$FileMonitoringController$1` to a named class to avoid log warnings related to serializing anonymous classes ([JENKINS-55145](https://issues.jenkins-ci.org/browse/JENKINS-55145)) 17 | - Internal: Fix CI build on Windows ([PR 125](https://github.com/jenkinsci/durable-task-plugin/pull/125)) 18 | 19 | ### Version 1.34 20 | 21 | Release date: 2020-03-10 22 | 23 | - Internal: Clean up deprecated code and unused imports ([PR-109](https://github.com/jenkinsci/durable-task-plugin/pull/109)) 24 | - Internal: Remove utility function for tempDir and use implementation from core instead ([PR-110](https://github.com/jenkinsci/durable-task-plugin/pull/110)) 25 | - Internal: Upgrade parent pom to 3.54 ([PR-119](https://github.com/jenkinsci/durable-task-plugin/pull/119)) 26 | - Fix: Explicitly close `ProcessPipeInputStream` to prevent agent-side OOM ([JENKINS-60960](https://issues.jenkins-ci.org/browse/JENKINS-60960)) 27 | - Internal: Make `generate-binaries` script return error. Refactor unit tests to support infra changes and to skip binary tests 28 | when binary is not generated ([PR-121](https://github.com/jenkinsci/durable-task-plugin/pull/121)) 29 | 30 | ### Version 1.33 31 | 32 | Release date: 2019-10-29 33 | 34 | - Disable binary wrapper caching when there are inadequate permissions to access the cache dir (namely containerized instances). 35 | ([JENKINS-59903](https://issues.jenkins-ci.org/browse/JENKINS-59903)) 36 | - Do not use binary wrapper on non-x86 and FreeBSD architectures ([JENKINS-59907](https://issues.jenkins-ci.org/browse/JENKINS-59907)) 37 | 38 | ### Version 1.32 39 | 40 | Release date: 2019-10-28 41 | 42 | > **WARNING**: The bugs introduced in 1.31 are still present (([JENKINS-59903](https://issues.jenkins-ci.org/browse/JENKINS-59903), 43 | > [JENKINS-59907](https://issues.jenkins-ci.org/browse/JENKINS-59907)) 44 | 45 | - Migrate changelog from wiki to github, add README ([PR \#113](https://github.com/jenkinsci/durable-task-plugin/pull/113)) 46 | - Disable binary wrapper (introduced in 1.31) by default. 47 | - To enable binary wrapper, pass the system property 48 | `org.jenkinsci.plugins.durabletask.BourneShellScript.FORCE_BINARY_WRAPPER=true` to the Java command line used to start Jenkins. 49 | 50 | ### Version 1.31 51 | 52 | Release date: 2019-10-22 53 | 54 | > **WARNING**: This version (1.31) introduced bugs where scripts will not be able to launch on non-x86 platforms and 55 | > container-based agents that do not have access to the agent node's root directory. 56 | 57 | > **NOTE**: To revert to previous release behavior, pass the system property 58 | > `org.jenkinsci.plugins.durabletask.BourneShellScript.FORCE_SHELL_WRAPPER=true` to the Java command line used to start Jenkins. 59 | 60 | - Update ssh-slaves ([PR \#100](https://github.com/jenkinsci/durable-task-plugin/pull/100)) 61 | - Do not fail tests when run on a machine without Docker installed. 62 | ([PR \#101](https://github.com/jenkinsci/durable-task-plugin/pull/101)) 63 | - Improve watcher logging 64 | ([PR \#102](https://github.com/jenkinsci/durable-task-plugin/pull/102)) 65 | - Refactor UNIX unit tests for greater test coverage 66 | ([PR \#103](https://github.com/jenkinsci/durable-task-plugin/pull/103)) 67 | - Allow setting pwsh as Powershell executable 68 | ([PR \#112](https://github.com/jenkinsci/durable-task-plugin/pull/111)) 69 | - Bugfix: Use setsid instead of nohup ([JENKINS-25503](https://issues.jenkins-ci.org/browse/JENKINS-25503)) 70 | - For \*NIX systems only, the shell wrapper has been replaced with a pre-compiled golang binary. 71 | - The binary launches the script under a new session to better survive unexpected Jenkins terminations. 72 | - Just like how the shell wrapper executes in the background (since 1.30), the script launcher 73 | is a daemonized process. The means that there is an expectation of orphaned-child cleanup 74 | (i.e. zombie-reaping) within the underlying environment. 75 | - The binary itself is \~2.5MB per binary. There are 4 pre-compiled binaries (32 and 64bit versions 76 | for UNIX and DARWIN). 77 | - The memory footprint is \~800KB heavier than the shell wrapper. 78 | - The two shell processes (610-640KB) and single sleep process (548KB) are replaced by a 79 | single process (\~2560KB) 80 | 81 | ### Version 1.30 82 | 83 | Release date: 2019-07-05 84 | 85 | - Bugfix: Run the wrapper process for shell scripts in the background. 86 | ([JENKINS-58290](https://issues.jenkins-ci.org/browse/JENKINS-58290)). This 87 | means that when the script exits, the wrapper process will be orphaned. In most cases, the 88 | orphaned process is cleaned up by the underlying OS (ie zombie-reaping). Special flags must 89 | be used to enable zombie-reaping in docker containers (--init) or kubernetes pods (shared 90 | process namespaces). 91 | - Bugfix: Use `sh` to run shell scripts rather than attempting to 92 | use the absolute path to the default shell from the master on 93 | agents. ([PR \#95](https://github.com/jenkinsci/durable-task-plugin/pull/95)) 94 | - Bugfix: Make PowerShell exit codes propagate correctly. Fixes a 95 | regression from version 1.23 96 | ([JENKINS-52884](https://issues.jenkins-ci.org/browse/JENKINS-52884)) 97 | 98 | ### Version 1.29 99 | 100 | Release date: 2019-01-31 101 | 102 | - Enhancement: Add support for z/OS Unix System Services to the `sh` 103 | step. ([JENKINS-37341](https://issues.jenkins-ci.org/browse/JENKINS-37341)) 104 | 105 | ### Version 1.28 106 | 107 | Release date: 2018-11-14 108 | 109 | - Bugfix: Do not rely on a shebang line to select the interpreter 110 | for `sh` scripts. This means that Pipelines can now use relative 111 | names for interpreters accessible from the `PATH` environment 112 | variable. 113 | ([JENKINS-50902](https://issues.jenkins-ci.org/browse/JENKINS-50902)) 114 | 115 | ### Version 1.27 116 | 117 | Release date: 2018-11-01 118 | 119 | - Do not print the working directory or the type of script being run 120 | when a durable task starts. ([PR \#83](https://github.com/jenkinsci/durable-task-plugin/pull/83)) 121 | - Internal: Shut down thread pools when Jenkins shuts down. Should 122 | only affect other plugins using this plugin in their tests. 123 | 124 | ### Version 1.26 125 | 126 | Release date: 2018-09-25 127 | 128 | - Bugfix: Increase the default heartbeat interval used to detect dead 129 | processes from 15 seconds to 5 minutes 130 | ([JENKINS-48300](https://issues.jenkins-ci.org/browse/JENKINS-48300)) 131 | - Developer: Define API for pushing durable task logs from build 132 | agents directly instead of having the Jenkins master pull logs from 133 | build agents 134 | ([JENKINS-52165](https://issues.jenkins-ci.org/browse/JENKINS-52165)) 135 | 136 | ### Version 1.25 137 | 138 | Release date: 2018-08-08 139 | 140 | - Major bugfix: Fix regressions in 1.23 and 1.24 that caused build 141 | failures when running `sh` steps in minimal environments such as 142 | Alpine and Cygwin 143 | ([JENKINS-52881](https://issues.jenkins-ci.org/browse/JENKINS-52881)) 144 | 145 | ### Version 1.24 146 | 147 | Release date: 2018-08-07 148 | 149 | - **(Warning: Fix is incomplete. Full fix is in version 1.25)** Major 150 | bugfix: Fix regression in 1.23 that caused build failures on 151 | Alpine-based build agents 152 | ([JENKINS-52847](https://issues.jenkins-ci.org/browse/JENKINS-52847)) 153 | - Developer: Define API for gathering command output in a local 154 | encoding 155 | ([JEP-206](https://github.com/jenkinsci/jep/blob/master/jep/206/README.adoc)) 156 | 157 | ### Version 1.23 158 | 159 | Release date: 2018-07-31 160 | 161 | - Major bugfix: properly count log content sent to avoid 162 | endlessly-repeating logs 163 | ([JENKINS-37575](https://issues.jenkins-ci.org/browse/JENKINS-37575)) 164 | - Bugfix: Ensure that the logfile-touching process exits when the 165 | wrapper script running the durable task dies 166 | ([JENKINS-50892](https://issues.jenkins-ci.org/browse/JENKINS-50892)) 167 | - Fix/Enhancement: Simplify Powershell execution and make it more 168 | consistent 169 | ([JENKINS-50840](https://issues.jenkins-ci.org/browse/JENKINS-50840)) 170 | - Admin: support incrementals 171 | 172 | ### Version 1.22 173 | 174 | Release date: 2018-03-13 175 | 176 | - Bugfix: Fix issues with Powershell error handling 177 | ([JENKINS-50029](https://issues.jenkins-ci.org/browse/JENKINS-50029)) 178 | 179 | ### Version 1.21 180 | 181 | Release date: 2018-03-08 182 | 183 | - Bugfix: Resolves regression with Batch steps "hanging" on some 184 | Windows build agents 185 | ([JENKINS-50025](https://issues.jenkins-ci.org/browse/JENKINS-50025)) 186 | introduced by 1.19 187 | - Fix for existing builds suffering the issue: go into the control 188 | directory in the workspace and run 'move jenkins-result.txt.tmp 189 | jenkins-result.txt' - the batch step will complete normally 190 | 191 | ### Version 1.20 192 | 193 | Release date: 2018-03-07 194 | 195 | - Bugfix: Prevent PowerShell stdout pollution when using returnStdout 196 | ([JENKINS-49754](https://issues.jenkins-ci.org/browse/JENKINS-49754)) 197 | 198 | ### Version 1.19 199 | 200 | Release date: 2018-03-07 201 | 202 | - Bugfix: Fix bogus DurableTask failures with "exit status code -1" 203 | due to non-atomic write of exit status code from processes 204 | ([JENKINS-25519](https://issues.jenkins-ci.org/browse/JENKINS-25519)) 205 | 206 | ### Version 1.18 207 | 208 | Release date: 2018-02-16 209 | 210 | - **Major Bug Fixes to PowerShell step:** 211 | - Incorrect exit 212 | codes ([JENKINS-46876](https://issues.jenkins-ci.org/browse/JENKINS-46876)) 213 | - Hanging 214 | ([JENKINS-46508](https://issues.jenkins-ci.org/browse/JENKINS-46508)) 215 | - Does not output UTF-8 byte order mark 216 | ([JENKINS-46496](https://issues.jenkins-ci.org/browse/JENKINS-46496)) 217 | - Does not show live output as of version 1.15 218 | ([JENKINS-48057](https://issues.jenkins-ci.org/browse/JENKINS-48057)) 219 | - Step always returns success 220 | ([JENKINS-47797](https://issues.jenkins-ci.org/browse/JENKINS-47797)) 221 | - Add passing of TaskListener API 222 | ([JENKINS-48300](https://issues.jenkins-ci.org/browse/JENKINS-48300)) 223 | - Test fixes 224 | 225 | ### Version 1.17 226 | 227 | Release date: 2017-11-21 228 | 229 | - Version 1.16 accidentally declared a Java dependency of 8+, despite 230 | being otherwise compatible with Jenkins 2.7.x+ which run on Java 7. 231 | Reverted to 7+. 232 | - Internal: improved resilience of Docker-based test suites. 233 | 234 | ### Version 1.16 235 | 236 | Release date: 2017-11-14 237 | 238 | > **WARNING**: This version (1.16) temporarily introduced a dependency on Java 8 which 239 | was reverted to Java 7 with version 1.17 240 | 241 | - [JENKINS-47791](https://issues.jenkins-ci.org/browse/JENKINS-47791): Using a new system for determining 242 | whether `sh` step processes are still alive, which should solve 243 | various robustness issues. 244 | - [JENKINS-46496](https://issues.jenkins-ci.org/browse/JENKINS-46496): Fixed BOM issue with the `powershell` step, perhaps 245 | 246 | ### Version 1.15 247 | 248 | Release date: 2017-10-13 249 | 250 | - Apply a timeout to checking when processes are started, so that we 251 | can't hang indefinitely 252 | 253 | ### Version 1.14 254 | 255 | Release date: 2017-06-15 256 | 257 | - JENKINS-34581 Powershell support. 258 | - JENKINS-43639 File descriptor leak. 259 | 260 | ### Version 1.13 261 | 262 | Release date: 2017-01-18 263 | 264 | - [JENKINS-40734](https://issues.jenkins-ci.org/browse/JENKINS-40734) 265 | Environment variable values containing `$` were not correctly passed 266 | to subprocesses. 267 | - [JENKINS-40225](https://issues.jenkins-ci.org/browse/JENKINS-40225) 268 | Replace backslashes when on Cygwin to allow `sh` to be used. 269 | 270 | > **WARNING**: Users setting node (or global) environment variables like 271 | `PATH=/something:$PATH` will see Pipeline `sh` failures with this update 272 | unless you also update the [Pipeline Nodes and Processes 273 | Plugin](https://wiki.jenkins.io/display/JENKINS/Pipeline+Nodes+and+Processes+Plugin) 274 | to 2.9 or later 275 | ([JENKINS-41339](https://issues.jenkins-ci.org/browse/JENKINS-41339)). 276 | Anyway you are advised to use the syntax `PATH+ANYKEY=/something`, as 277 | documented in inline help. 278 | 279 | ### Version 1.12 280 | 281 | Release date: 2016-07-28 282 | 283 | - Infrastructure for 284 | [JENKINS-26133](https://issues.jenkins-ci.org/browse/JENKINS-26133). 285 | 286 | ### Version 1.11 287 | 288 | Release date: 2016-06-29 289 | 290 | - Infrastructure for 291 | [JENKINS-31842](https://issues.jenkins-ci.org/browse/JENKINS-31842). 292 | 293 | ### Version 1.10 294 | 295 | Release date: 2016-05-19 296 | 297 | - [JENKINS-34150](https://issues.jenkins-ci.org/browse/JENKINS-34150) 298 | `bat` hangs under some conditions. 299 | 300 | ### Version 1.9 301 | 302 | Release date: 2016-03-24 303 | 304 | - [JENKINS-32701](https://issues.jenkins-ci.org/browse/JENKINS-32701) 305 | Handle percent signs in the working directory for batch scripts, for 306 | example due to a Pipeline branch project based on a Git branch with 307 | a `/` in its name. 308 | 309 | ### Version 1.8 310 | 311 | Release date: 2016-03-03 312 | 313 | - [JENKINS-27152](https://issues.jenkins-ci.org/browse/JENKINS-27152) 314 | Store control directory outside of the workspace. 315 | - [JENKINS-28400](https://issues.jenkins-ci.org/browse/JENKINS-28400) 316 | Better diagnostics when wrapper shell script fails to start. 317 | - [JENKINS-25678](https://issues.jenkins-ci.org/browse/JENKINS-25678) 318 | Refinement of fix in 1.4. 319 | 320 | ### Version 1.8-beta-1 321 | 322 | Release date: 2016-01-19 323 | 324 | - [JENKINS-32264](https://issues.jenkins-ci.org/browse/JENKINS-32264) 325 | Linux-only process liveness check broke usage on FreeBSD. 326 | 327 | ### Version 1.7 328 | 329 | Release date: 2015-12-03 330 | 331 | - [JENKINS-27152](https://issues.jenkins-ci.org/browse/JENKINS-27152) 332 | Not a fix, but use a more predictable control directory name. 333 | - [JENKINS-27419](https://issues.jenkins-ci.org/browse/JENKINS-27419) 334 | Handle batch scripts that `exit` without `/b`. 335 | 336 | ### Version 1.6 337 | 338 | Release date: 2015-04-08 339 | 340 | - Do not kill a one-shot agent merely because a flyweight task 341 | happened to run on it (rather than on master as usual). Works around 342 | a bug in the Multibranch API plugin. 343 | 344 | ### Version 1.5 345 | 346 | Release date: 2015-05-04 347 | 348 | - Requires Jenkins 1.565.3+. 349 | - Richer API for launching and stopping processes. 350 | 351 | ### Version 1.4 352 | 353 | Release date: 2015-03-06 354 | 355 | - [JENKINS-25678](https://issues.jenkins-ci.org/browse/JENKINS-25678) 356 | Space-in-path bug affecting Windows builds. 357 | 358 | ### Version 1.3 359 | 360 | Release date: 2015-02-02 361 | 362 | - Continuing to try to fix deadlocks. 363 | 364 | ### Version 1.2 365 | 366 | Release date: 2015-01-13 367 | 368 | - [JENKINS-26380](https://issues.jenkins-ci.org/browse/JENKINS-26380) 369 | Occasional deadlocks when running against Jenkins 1.592+. 370 | 371 | ### Version 1.1 372 | 373 | Release date: 2014-12-05 374 | 375 | - [JENKINS-25848](https://issues.jenkins-ci.org/browse/JENKINS-25848) 376 | Failure to run shell tasks on some Mac OS X installations. 377 | 378 | ### Version 1.0 379 | 380 | Release date: 2014-11-25 381 | 382 | - [JENKINS-25727](https://issues.jenkins-ci.org/browse/JENKINS-25727) 383 | Race condition causing spurious -1 exit codes, especially for 384 | short-lived shell scripts. 385 | - Print a warning when asked to run an empty shell script. 386 | - Avoid allocating a useless thread on the agent while running a task. 387 | 388 | ### Version 0.7 389 | 390 | Release date: 2014-10-10 391 | 392 | - [JENKINS-25727](https://issues.jenkins-ci.org/browse/JENKINS-25727) 393 | Better reliability of liveness checker when short-lived scripts are 394 | being run. (Amended in 1.0.) 395 | 396 | ### Version 0.6 397 | 398 | Release date: 2014-10-08 399 | 400 | - [JENKINS-22249](https://issues.jenkins-ci.org/browse/JENKINS-22249) 401 | Detect if the wrapper shell script is dead, for example because the 402 | machine was rebooted. 403 | - Efficiency improvements in log copying. 404 | 405 | ### Version 0.5 406 | 407 | Release date: 2014-09-24 408 | 409 | - New APIs `ContinuableExecutable` and `OnceRetentionStrategy`. 410 | - Moved `ContinuedTask` into a subpackage. 411 | 412 | ### Version 0.4 413 | 414 | Release date: 2014-08-27 415 | 416 | - New API: 417 | - Better error handling. 418 | - [JENKINS-23027](https://issues.jenkins-ci.org/browse/JENKINS-23027) 419 | Print nicer output when running on newer versions of Jenkins. 420 | 421 | ### Version 0.3 422 | 423 | Release date: 2014-07-22 424 | 425 | - Supporting `java.io.Serializable`. 426 | 427 | ### Version 0.2 428 | 429 | Release date: 2014-05-29 430 | 431 | - [JENKINS-22248](https://issues.jenkins-ci.org/browse/JENKINS-22248) 432 | Allow multiple scripts to run in the same workspace concurrently. 433 | - Allow a “shell” script to override the interpreter. 434 | - Use the default configured shell. 435 | - Start the shell with `-xe` (echo commands, fail on error). 436 | 437 | ### Version 0.1 438 | 439 | Release date: 2014-03-18 440 | 441 | - Initial release. 442 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin(configurations: [ 2 | [platform: 'linux', jdk: 21], 3 | [platform: 'windows', jdk: 17], 4 | ]) 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Durable Task Plugin 2 | === 3 | 4 | Library offering an extension point for processes which can run outside 5 | of Jenkins yet be monitored. 6 | 7 | Offers no direct features on its own but can be used by other feature 8 | plugins. 9 | 10 | ## Documentation 11 | 12 | * [Changelog](https://github.com/jenkinsci/durable-task-plugin/releases) 13 | * [Example](https://github.com/jenkinsci/workflow-durable-task-step-plugin) 14 | * [Blog post](https://web.archive.org/web/20141227025217/http://tupilabs.com/2014/06/13/durable-tasks-in-jenkins.html) 15 | 16 | ## License 17 | 18 | [MIT License](https://opensource.org/licenses/mit-license.php) 19 | 20 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.jenkins-ci.plugins 5 | plugin 6 | 5.10 7 | 8 | 9 | durable-task 10 | ${changelist} 11 | hpi 12 | Durable Task Plugin 13 | Library offering an extension point for processes which can run outside of Jenkins yet be monitored. 14 | https://github.com/jenkinsci/${project.artifactId}-plugin 15 | 16 | 17 | MIT License 18 | https://opensource.org/licenses/MIT 19 | 20 | 21 | 22 | 23 | 999999-SNAPSHOT 24 | 25 | 2.479 26 | ${jenkins.baseline}.1 27 | jenkinsci/${project.artifactId}-plugin 28 | 29 | 30 | 31 | 32 | repo.jenkins-ci.org 33 | https://repo.jenkins-ci.org/public/ 34 | 35 | 36 | 37 | 38 | repo.jenkins-ci.org 39 | https://repo.jenkins-ci.org/public/ 40 | 41 | 42 | 43 | 44 | scm:git:https://github.com/${gitHubRepo}.git 45 | scm:git:git@github.com:${gitHubRepo}.git 46 | https://github.com/${gitHubRepo} 47 | ${scmTag} 48 | 49 | 50 | 51 | io.jenkins.plugins 52 | lib-durable-task 53 | 48.v72b_86b_9ca_8e1 54 | 55 | 56 | org.jenkins-ci.test 57 | docker-fixtures 58 | 200.v22a_e8766731c 59 | test 60 | 61 | 62 | org.jenkins-ci.plugins 63 | ssh-slaves 64 | test 65 | 66 | 67 | org.jenkins-ci.plugins.workflow 68 | workflow-job 69 | test 70 | 71 | 72 | org.jenkins-ci.plugins.workflow 73 | workflow-durable-task-step 74 | test 75 | 76 | 77 | org.jenkins-ci.plugins.workflow 78 | workflow-cps 79 | test 80 | 81 | 82 | org.awaitility 83 | awaitility 84 | 4.3.0 85 | test 86 | 87 | 88 | 89 | 90 | 91 | io.jenkins.tools.bom 92 | bom-${jenkins.baseline}.x 93 | 3893.v213a_42768d35 94 | pom 95 | import 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-surefire-plugin 105 | 106 | 107 | 108 | org.apache.maven.surefire 109 | surefire-junit47 110 | ${maven-surefire-plugin.version} 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/durabletask/AgentInfo.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.plugins.durabletask; 2 | import java.io.File; 3 | import java.io.IOException; 4 | import java.io.Serializable; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.nio.file.Paths; 8 | 9 | import jenkins.MasterToSlaveFileCallable; 10 | import org.jenkinsci.remoting.RoleChecker; 11 | 12 | import hudson.Platform; 13 | import hudson.FilePath.FileCallable; 14 | import hudson.remoting.VirtualChannel; 15 | 16 | public final class AgentInfo implements Serializable { 17 | private static final long serialVersionUID = 7599995179651071957L; 18 | private final OsType os; 19 | private final String binaryPath; 20 | private final String architecture; 21 | private boolean binaryCompatible; 22 | private boolean binaryCached; 23 | private boolean cachingAvailable; 24 | 25 | public enum OsType { 26 | DARWIN("darwin"), 27 | LINUX("linux"), 28 | WINDOWS("win"), 29 | FREEBSD("freebsd"), 30 | ZOS("zos"), 31 | UNKNOWN("unknown"); 32 | 33 | private final String binaryName; 34 | OsType(final String binaryName) { 35 | this.binaryName = binaryName; 36 | } 37 | public String getNameForBinary() { 38 | return binaryName; 39 | } 40 | } 41 | 42 | public AgentInfo(OsType os, String architecture, boolean binaryCompatible, String binaryPath, boolean cachingAvailable) { 43 | this.os = os; 44 | this.architecture = architecture; 45 | this.binaryPath = binaryPath; 46 | this.binaryCompatible = binaryCompatible; 47 | this.binaryCached = false; 48 | this.cachingAvailable = cachingAvailable; 49 | } 50 | 51 | public OsType getOs() { 52 | return os; 53 | } 54 | 55 | public String getArchitecture() { 56 | return architecture; 57 | } 58 | 59 | public String getBinaryPath() { 60 | return binaryPath; 61 | } 62 | 63 | public void setBinaryAvailability(boolean isCached) { 64 | binaryCached = isCached; 65 | } 66 | 67 | public boolean isBinaryCompatible() { 68 | return binaryCompatible; 69 | } 70 | 71 | public boolean isBinaryCached() { 72 | return binaryCached; 73 | } 74 | 75 | public boolean isCachingAvailable() { 76 | return cachingAvailable; 77 | } 78 | 79 | public static final class GetAgentInfo extends MasterToSlaveFileCallable { 80 | private static final long serialVersionUID = 1L; 81 | private static final String BINARY_PREFIX = "durable_task_monitor_"; 82 | private static final String CACHE_PATH = "caches/durable-task/"; 83 | private static final String NOT_SUPPORTED = "NOTSUPPORTED"; 84 | // Version makes sure we don't use an out-of-date cached binary 85 | private String binaryVersion; 86 | 87 | GetAgentInfo(String pluginVersion) { 88 | this.binaryVersion = pluginVersion; 89 | } 90 | 91 | @Override 92 | public AgentInfo invoke(File nodeRoot, VirtualChannel virtualChannel) throws IOException, InterruptedException { 93 | OsType os; 94 | if (Platform.isDarwin()) { 95 | os = OsType.DARWIN; 96 | } else if (Platform.current() == Platform.WINDOWS) { 97 | os = OsType.WINDOWS; 98 | } else { 99 | String osName = System.getProperty("os.name"); 100 | if (osName.equalsIgnoreCase("linux")) { 101 | os = OsType.LINUX; 102 | } else if (osName.equalsIgnoreCase("z/OS")) { 103 | os = OsType.ZOS; 104 | } else if (osName.equalsIgnoreCase("FreeBSD")) { 105 | os = OsType.FREEBSD; 106 | } else { 107 | os = OsType.UNKNOWN; 108 | } 109 | } 110 | 111 | String arch = System.getProperty("os.arch"); 112 | String archType = ""; 113 | if (os == OsType.DARWIN) { 114 | if (arch.contains("aarch") || arch.contains("arm")) { 115 | archType = "arm"; 116 | } else if (arch.contains("amd") || arch.contains("x86")) { 117 | archType = "amd"; 118 | } else { 119 | archType = NOT_SUPPORTED; 120 | } 121 | } 122 | 123 | if (os == OsType.LINUX) { 124 | archType = arch; 125 | switch (arch) { 126 | case "aarch64": 127 | case "ppc64le": 128 | archType = arch; 129 | break; 130 | case "amd64": 131 | archType = "64"; 132 | break; 133 | case "x86": 134 | archType = "32"; 135 | break; 136 | default: 137 | archType = NOT_SUPPORTED; 138 | } 139 | } else { 140 | // Note: This will only determine the architecture bits of the JVM. 141 | String bits = System.getProperty("sun.arch.data.model"); 142 | if (bits.equals("64") || bits.equals("32")) { 143 | archType += bits; 144 | } else { 145 | archType += NOT_SUPPORTED; 146 | } 147 | } 148 | 149 | boolean binaryCompatible; 150 | if ((os == OsType.DARWIN) || (os == OsType.LINUX) || (os == OsType.WINDOWS) && !archType.contains(NOT_SUPPORTED)) { 151 | binaryCompatible = true; 152 | } else { 153 | binaryCompatible = false; 154 | } 155 | 156 | String extension = ""; 157 | if (os == OsType.WINDOWS) { 158 | extension = ".exe"; 159 | } 160 | 161 | String binaryName = BINARY_PREFIX + binaryVersion + "_" + os.getNameForBinary() + "_" + archType + extension; 162 | String binaryPath; 163 | boolean isCached; 164 | boolean cachingAvailable; 165 | try { 166 | Path cachePath = Paths.get(nodeRoot.toPath().toString(), CACHE_PATH); 167 | Files.createDirectories(cachePath); 168 | File binaryFile = new File(cachePath.toFile(), binaryName); 169 | binaryPath = binaryFile.toPath().toString(); 170 | isCached = binaryFile.exists(); 171 | cachingAvailable = true; 172 | } catch (Exception e) { 173 | // when the jenkins agent cache path is not accessible 174 | binaryPath = binaryName; 175 | isCached = false; 176 | cachingAvailable = false; 177 | } 178 | AgentInfo agentInfo = new AgentInfo(os, archType, binaryCompatible, binaryPath, cachingAvailable); 179 | agentInfo.setBinaryAvailability(isCached); 180 | return agentInfo; 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/durabletask/BourneShellScript.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2014 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.durabletask; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 29 | import hudson.EnvVars; 30 | import hudson.Extension; 31 | import hudson.FilePath; 32 | import hudson.Launcher; 33 | import hudson.PluginWrapper; 34 | import hudson.Proc; 35 | import hudson.Util; 36 | import hudson.model.TaskListener; 37 | import hudson.remoting.VirtualChannel; 38 | import hudson.tasks.Shell; 39 | import java.io.IOException; 40 | import java.io.InputStream; 41 | import java.io.File; 42 | import java.nio.charset.Charset; 43 | import java.util.ArrayList; 44 | import java.util.Arrays; 45 | import java.util.List; 46 | import java.util.concurrent.TimeUnit; 47 | import java.util.logging.Level; 48 | import java.util.logging.Logger; 49 | import edu.umd.cs.findbugs.annotations.NonNull; 50 | 51 | import jenkins.model.Jenkins; 52 | import jenkins.security.MasterToSlaveCallable; 53 | import org.apache.commons.lang.StringUtils; 54 | import org.jenkinsci.plugins.durabletask.AgentInfo.OsType; 55 | import org.kohsuke.accmod.Restricted; 56 | import org.kohsuke.accmod.restrictions.NoExternalUse; 57 | import org.kohsuke.stapler.DataBoundConstructor; 58 | import edu.umd.cs.findbugs.annotations.CheckForNull; 59 | import edu.umd.cs.findbugs.annotations.Nullable; 60 | import jenkins.MasterToSlaveFileCallable; 61 | 62 | /** 63 | * Runs a Bourne shell script on a Unix node using {@code nohup}. 64 | */ 65 | public final class BourneShellScript extends FileMonitoringTask { 66 | 67 | @SuppressFBWarnings("MS_SHOULD_BE_FINAL") // Used to control usage of binary or shell wrapper 68 | @Restricted(NoExternalUse.class) 69 | public static boolean USE_BINARY_WRAPPER = Boolean.getBoolean(BourneShellScript.class.getName() + ".USE_BINARY_WRAPPER"); 70 | 71 | private static final Logger LOGGER = Logger.getLogger(BourneShellScript.class.getName()); 72 | 73 | private static final String SYSTEM_DEFAULT_CHARSET = "SYSTEM_DEFAULT"; 74 | 75 | private static final String LAUNCH_DIAGNOSTICS_PROP = BourneShellScript.class.getName() + ".LAUNCH_DIAGNOSTICS"; 76 | 77 | /** 78 | * Whether to stream stdio from the wrapper script, which should normally not print any. 79 | * Copying output from the controller process consumes a Java thread, so we want to avoid it generally. 80 | * If requested, we can do this to assist in diagnosis. 81 | * (For example, if we are unable to write to a workspace due to permissions, 82 | * we would want to see that error message.) 83 | * 84 | * For the binary wrapper, this enables the debug flag. 85 | */ 86 | @SuppressWarnings("FieldMayBeFinal") 87 | // TODO use SystemProperties if and when unrestricted 88 | private static boolean LAUNCH_DIAGNOSTICS = Boolean.getBoolean(LAUNCH_DIAGNOSTICS_PROP); 89 | 90 | /** 91 | * Seconds between heartbeat checks, where we check to see if 92 | * {@code jenkins-log.txt} is still being modified. 93 | */ 94 | static int HEARTBEAT_CHECK_INTERVAL = Integer.getInteger(BourneShellScript.class.getName() + ".HEARTBEAT_CHECK_INTERVAL", 300); 95 | 96 | /** 97 | * Minimum timestamp difference on {@code jenkins-log.txt} that is 98 | * considered an actual modification. Theoretically could be zero (if 99 | * {@code <} became {@code <=}, else infinitesimal positive) but on some 100 | * platforms file timestamps are not that precise. 101 | */ 102 | @SuppressWarnings("FieldMayBeFinal") 103 | private static int HEARTBEAT_MINIMUM_DELTA = Integer.getInteger(BourneShellScript.class.getName() + ".HEARTBEAT_MINIMUM_DELTA", 2); 104 | 105 | private final @NonNull String script; 106 | private boolean capturingOutput; 107 | 108 | @DataBoundConstructor public BourneShellScript(String script) { 109 | this.script = Util.fixNull(script); 110 | } 111 | 112 | public String getScript() { 113 | return script; 114 | } 115 | 116 | @Override public void captureOutput() { 117 | capturingOutput = true; 118 | } 119 | 120 | @Override protected FileMonitoringController launchWithCookie(FilePath ws, Launcher launcher, TaskListener listener, EnvVars envVars, String cookieVariable, String cookieValue) throws IOException, InterruptedException { 121 | if (script.isEmpty()) { 122 | listener.getLogger().println("Warning: was asked to run an empty script"); 123 | } 124 | 125 | FilePath nodeRoot = getNodeRoot(ws); 126 | AgentInfo agentInfo = getAgentInfo(nodeRoot); 127 | 128 | OsType os = agentInfo.getOs(); 129 | String scriptEncodingCharset = "UTF-8"; 130 | String jenkinsResultTxtEncoding = null; 131 | if(os == OsType.ZOS) { 132 | Charset zOSSystemEncodingCharset = Charset.forName(ws.act(new getIBMzOsEncoding())); 133 | if(SYSTEM_DEFAULT_CHARSET.equals(getCharset())) { 134 | // Setting default charset to IBM z/OS default EBCDIC charset on z/OS if no encoding specified on sh step 135 | charset(zOSSystemEncodingCharset); 136 | } 137 | scriptEncodingCharset = zOSSystemEncodingCharset.name(); 138 | jenkinsResultTxtEncoding = zOSSystemEncodingCharset.name(); 139 | } 140 | 141 | ShellController c = new ShellController(ws,(os == OsType.ZOS), cookieValue, jenkinsResultTxtEncoding); 142 | FilePath shf = c.getScriptFile(ws); 143 | 144 | // JENKINS-70874: if a new process is forked during this call, the writeable file handle will be copied and leading to the "Text file busy" issue 145 | // when executing the script. 146 | shf.write(script, scriptEncodingCharset); 147 | 148 | String shell = null; 149 | if (!script.startsWith("#!")) { 150 | shell = Jenkins.get().getDescriptorByType(Shell.DescriptorImpl.class).getShell(); 151 | if (shell == null) { 152 | // Do not use getShellOrDefault, as that assumes that the filesystem layout of the agent matches that seen from a possibly decorated launcher. 153 | shell = "sh"; 154 | } 155 | } else { 156 | shf.chmod(0755); 157 | } 158 | 159 | String scriptPath = shf.getRemote(); 160 | 161 | // The temporary variable is to ensure JENKINS_SERVER_COOKIE=durable-… does not appear even in argv[], lest it be confused with the environment. 162 | envVars.put(cookieVariable, "please-do-not-kill-me"); 163 | 164 | List launcherCmd = null; 165 | FilePath binary; 166 | if (USE_BINARY_WRAPPER && (binary = requestBinary(nodeRoot, agentInfo, ws, c)) != null) { 167 | launcherCmd = binaryLauncherCmd(c, ws, shell, binary.getRemote(), scriptPath, cookieValue, cookieVariable); 168 | } 169 | if (launcherCmd == null) { 170 | launcherCmd = scriptLauncherCmd(c, ws, shell, os, scriptPath, cookieValue, cookieVariable); 171 | } 172 | 173 | LOGGER.log(Level.FINE, "launching {0}", launcherCmd); 174 | Launcher.ProcStarter ps = launcher.launch().cmds(launcherCmd).envs(escape(envVars)).pwd(ws).quiet(true); 175 | if (LAUNCH_DIAGNOSTICS) { 176 | ps.stdout(listener); 177 | ps.start(); 178 | } else { 179 | ps.readStdout().readStderr(); // TODO RemoteLauncher.launch fails to check ps.stdout == NULL_OUTPUT_STREAM, so it creates a useless thread even if you never called stdout(…) 180 | Proc p = ps.start(); 181 | // Make sure these stream will get closed later, to release their remote counterpart from the agent's ExportTable. See JENKINS-60960. 182 | c.registerForCleanup(p.getStdout()); 183 | c.registerForCleanup(p.getStderr()); 184 | } 185 | return c; 186 | } 187 | 188 | @NonNull 189 | private List binaryLauncherCmd(ShellController c, FilePath ws, @Nullable String shell, String binaryPath, 190 | String scriptPath, String cookieValue, String cookieVariable) throws IOException, InterruptedException { 191 | String logFile = c.getLogFile(ws).getRemote(); 192 | String resultFile = c.getResultFile(ws).getRemote(); 193 | String outputFile = c.getOutputFile(ws).getRemote(); 194 | String controlDirPath = c.controlDir(ws).getRemote(); 195 | 196 | List cmd = new ArrayList<>(); 197 | cmd.add(binaryPath); 198 | cmd.add("-controldir=" + controlDirPath); 199 | cmd.add("-result=" + resultFile); 200 | cmd.add("-log=" + logFile); 201 | cmd.add("-cookiename=" + cookieVariable); 202 | cmd.add("-cookieval=" + cookieValue); 203 | cmd.add("-script=" + scriptPath); 204 | if (shell != null) { 205 | cmd.add("-shell=" + shell); 206 | } 207 | if (capturingOutput) { 208 | cmd.add("-output=" + outputFile); 209 | } 210 | // JENKINS-58290: launch in the background. No need to close stdout/err, binary does not write to them. 211 | cmd.add("-daemon"); 212 | if (LAUNCH_DIAGNOSTICS) { 213 | cmd.add("-debug"); 214 | } 215 | return cmd; 216 | } 217 | 218 | @NonNull 219 | private List scriptLauncherCmd(ShellController c, FilePath ws, @CheckForNull String shell, 220 | OsType os, String scriptPath, String cookieValue, 221 | String cookieVariable) throws IOException, InterruptedException { 222 | String cmdString; 223 | FilePath logFile = c.getLogFile(ws); 224 | FilePath resultFile = c.getResultFile(ws); 225 | FilePath controlDir = c.controlDir(ws); 226 | String interpreter = ""; 227 | 228 | if ((shell != null) && !script.startsWith("#!")) { 229 | interpreter = "'" + shell + "' -xe "; 230 | } 231 | if (os == OsType.WINDOWS) { // JENKINS-40255 232 | scriptPath = scriptPath.replace("\\", "/"); // cygwin sh understands mixed path (ie : "c:/jenkins/workspace/script.sh" ) 233 | } 234 | String scriptPathCopy = scriptPath + ".copy"; // copy file to protect against "Text file busy", see JENKINS-70874 235 | if (capturingOutput) { 236 | cmdString = String.format("cp '%s' '%s'; { while [ -d '%s' -a \\! -f '%s' ]; do touch '%s'; sleep 3; done } & jsc=%s; %s=$jsc %s '%s' > '%s' 2> '%s'; echo $? > '%s.tmp'; mv '%s.tmp' '%s'; wait", 237 | scriptPath, 238 | scriptPathCopy, 239 | controlDir, 240 | resultFile, 241 | logFile, 242 | cookieValue, 243 | cookieVariable, 244 | interpreter, 245 | scriptPathCopy, 246 | c.getOutputFile(ws), 247 | logFile, 248 | resultFile, resultFile, resultFile); 249 | } else { 250 | cmdString = String.format("cp '%s' '%s'; { while [ -d '%s' -a \\! -f '%s' ]; do touch '%s'; sleep 3; done } & jsc=%s; %s=$jsc %s '%s' > '%s' 2>&1; echo $? > '%s.tmp'; mv '%s.tmp' '%s'; wait", 251 | scriptPath, 252 | scriptPathCopy, 253 | controlDir, 254 | resultFile, 255 | logFile, 256 | cookieValue, 257 | cookieVariable, 258 | interpreter, 259 | scriptPathCopy, 260 | logFile, 261 | resultFile, resultFile, resultFile); 262 | } 263 | 264 | cmdString = cmdString.replace("$", "$$"); // escape against EnvVars jobEnv in LocalLauncher.launch 265 | List cmd = new ArrayList<>(); 266 | if (os != OsType.DARWIN && os != OsType.WINDOWS) { // JENKINS-25848 JENKINS-33708 267 | cmd.add("nohup"); 268 | } 269 | if (LAUNCH_DIAGNOSTICS) { 270 | cmd.addAll(Arrays.asList("sh", "-c", cmdString)); 271 | } else { 272 | // JENKINS-58290: launch in the background. Also close stdout/err so docker-exec and the like do not wait. 273 | cmd.addAll(Arrays.asList("sh", "-c", "(" + cmdString + ") >&- 2>&- &")); 274 | } 275 | return cmd; 276 | } 277 | 278 | /*package*/ static final class ShellController extends FileMonitoringController { 279 | 280 | /** Last time we checked the timestamp, in nanoseconds on the master. */ 281 | private transient long lastCheck; 282 | /** Last-observed modification time of {@link FileMonitoringTask.FileMonitoringController#getLogFile(FilePath)} on remote computer, in milliseconds. */ 283 | private transient long checkedTimestamp; 284 | 285 | /** Caching zOS flag to avoid round trip calls in exitStatus() */ 286 | private final boolean isZos; 287 | /** Encoding of jenkins-result.txt if on z/OS, null otherwise */ 288 | private String jenkinsResultTxtEncoding; 289 | 290 | private ShellController(FilePath ws, boolean zOsFlag, @NonNull String cookieValue, String jenkinsResultTxtEncoding) throws IOException, InterruptedException { 291 | super(ws, cookieValue); 292 | this.isZos = zOsFlag; 293 | this.jenkinsResultTxtEncoding = jenkinsResultTxtEncoding; 294 | } 295 | 296 | public FilePath getScriptFile(FilePath ws) throws IOException, InterruptedException { 297 | return controlDir(ws).child("script.sh"); 298 | } 299 | 300 | /** Only here for compatibility. */ 301 | private FilePath pidFile(FilePath ws) throws IOException, InterruptedException { 302 | return controlDir(ws).child("pid"); 303 | } 304 | 305 | @Override protected Integer exitStatus(FilePath workspace, TaskListener listener) throws IOException, InterruptedException { 306 | Integer status; 307 | if(isZos) { 308 | // We need to transcode status file from EBCDIC only on z/OS platform 309 | FilePath statusFile = getResultFile(workspace); 310 | status = statusFile.act(new StatusCheckWithEncoding(jenkinsResultTxtEncoding != null ? jenkinsResultTxtEncoding : getCharset())); 311 | } 312 | else { 313 | status = super.exitStatus(workspace, listener); 314 | } 315 | if (status != null) { 316 | LOGGER.log(Level.FINE, "found exit code {0} in {1}", new Object[] {status, controlDir}); 317 | return status; 318 | } 319 | long now = System.nanoTime(); 320 | if (lastCheck == 0) { 321 | LOGGER.log(Level.FINE, "starting check in {0}", controlDir); 322 | lastCheck = now; 323 | } else if (now > lastCheck + TimeUnit.SECONDS.toNanos(HEARTBEAT_CHECK_INTERVAL)) { 324 | lastCheck = now; 325 | long currentTimestamp = getLogFile(workspace).lastModified(); 326 | if (currentTimestamp == 0) { 327 | listener.getLogger().println("process apparently never started in " + controlDir); 328 | if (!LAUNCH_DIAGNOSTICS) { 329 | listener.getLogger().println("(running Jenkins temporarily with -D" + LAUNCH_DIAGNOSTICS_PROP + "=true might make the problem clearer)"); 330 | } 331 | return recordExitStatus(workspace, -2); 332 | } else if (checkedTimestamp > 0) { 333 | if (currentTimestamp < checkedTimestamp) { 334 | listener.getLogger().println("apparent clock skew in " + controlDir); 335 | } else if (currentTimestamp < checkedTimestamp + TimeUnit.SECONDS.toMillis(HEARTBEAT_MINIMUM_DELTA)) { 336 | FilePath pidFile = pidFile(workspace); 337 | if (pidFile.exists()) { 338 | listener.getLogger().println("still have " + pidFile + " so heartbeat checks unreliable; process may or may not be alive"); 339 | } else { 340 | listener.getLogger().println("wrapper script does not seem to be touching the log file in " + controlDir); 341 | listener.getLogger().println("(JENKINS-48300: if on an extremely laggy filesystem, consider -Dorg.jenkinsci.plugins.durabletask.BourneShellScript.HEARTBEAT_CHECK_INTERVAL=86400)"); 342 | return recordExitStatus(workspace, -1); 343 | } 344 | } 345 | } else { 346 | LOGGER.log(Level.FINE, "seeing recent log file modifications in {0}", controlDir); 347 | } 348 | checkedTimestamp = currentTimestamp; 349 | } 350 | return null; 351 | } 352 | 353 | private int recordExitStatus(FilePath workspace, int code) throws IOException, InterruptedException { 354 | getResultFile(workspace).write(Integer.toString(code), null); 355 | return code; 356 | } 357 | 358 | private static final long serialVersionUID = 1L; 359 | } 360 | 361 | @Extension public static final class DescriptorImpl extends DurableTaskDescriptor { 362 | 363 | @Override public String getDisplayName() { 364 | return Messages.BourneShellScript_bourne_shell(); 365 | } 366 | 367 | } 368 | 369 | private static final class getIBMzOsEncoding extends MasterToSlaveCallable { 370 | @Override public String call() throws RuntimeException { 371 | // Not null on z/OS systems 372 | return System.getProperty("ibm.system.encoding"); 373 | } 374 | private static final long serialVersionUID = 1L; 375 | } 376 | 377 | /* Local copy of StatusCheck to run on z/OS */ 378 | static class StatusCheckWithEncoding extends MasterToSlaveFileCallable { 379 | private final String charset; 380 | StatusCheckWithEncoding(String charset) { 381 | this.charset = charset; 382 | } 383 | @Override 384 | @CheckForNull 385 | public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { 386 | if (f.exists() && f.length() > 0) { 387 | try { 388 | String fileString = com.google.common.io.Files.readFirstLine(f, Charset.forName(charset)); 389 | if (fileString == null || fileString.isEmpty()) { 390 | return null; 391 | } else { 392 | fileString = fileString.trim(); 393 | if (fileString.isEmpty()) { 394 | return null; 395 | } else { 396 | return Integer.parseInt(fileString); 397 | } 398 | } 399 | } catch (NumberFormatException x) { 400 | throw new IOException("corrupted content in " + f + " using " + charset + ": " + x, x); 401 | } 402 | } 403 | return null; 404 | } 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/durabletask/ContinuedTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2014 Jesse Glick. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.durabletask; 26 | 27 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 28 | 29 | @Deprecated 30 | @SuppressFBWarnings(value = "NM_SAME_SIMPLE_NAME_AS_INTERFACE", justification = "Already deprecated.") 31 | public interface ContinuedTask extends org.jenkinsci.plugins.durabletask.executors.ContinuedTask {} 32 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/durabletask/Controller.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2014 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.durabletask; 26 | 27 | import hudson.FilePath; 28 | import hudson.Launcher; 29 | import hudson.Util; 30 | import hudson.model.TaskListener; 31 | import hudson.remoting.ChannelClosedException; 32 | import hudson.util.LogTaskListener; 33 | import java.io.IOException; 34 | import java.io.OutputStream; 35 | import java.io.Serializable; 36 | import java.util.logging.Level; 37 | import java.util.logging.Logger; 38 | import edu.umd.cs.findbugs.annotations.CheckForNull; 39 | import edu.umd.cs.findbugs.annotations.NonNull; 40 | 41 | /** 42 | * Defines how to control the execution of a task after it has started. 43 | * Expected to be XStream and Java serializable. 44 | */ 45 | public abstract class Controller implements Serializable { 46 | 47 | /** 48 | * Begins watching the process asynchronously, so that the master may receive notification when output is available or the process has exited. 49 | * This should be called as soon as the process is launched, and thereafter whenever reconnecting to the agent. 50 | * You should not call {@link #writeLog} or {@link #cleanup} in this case; you do not need to call {@link #exitStatus(FilePath, Launcher)} frequently, 51 | * though it is advisable to still call it occasionally to verify that the process is still running. 52 | * @param workspace the workspace in use 53 | * @param handler a remotable callback 54 | * @param listener a remotable destination for messages 55 | * @throws IOException if initiating the watch fails, for example with a {@link ChannelClosedException} 56 | * @throws UnsupportedOperationException when this mode is not available, so you must fall back to polling {@link #writeLog} and {@link #exitStatus(FilePath, Launcher)} 57 | */ 58 | public void watch(@NonNull FilePath workspace, @NonNull Handler handler, @NonNull TaskListener listener) throws IOException, InterruptedException, UnsupportedOperationException { 59 | throw new UnsupportedOperationException("Asynchronous mode is not implemented in " + getClass().getName()); 60 | } 61 | 62 | /** 63 | * Obtains any new task log output. 64 | * Could use a serializable field to keep track of how much output has been previously written. 65 | * @param workspace the workspace in use 66 | * @param sink where to send new log output 67 | * @return true if something was written and the controller should be resaved, false if everything is idle 68 | * @see DurableTask#charset 69 | * @see DurableTask#defaultCharset 70 | */ 71 | public abstract boolean writeLog(FilePath workspace, OutputStream sink) throws IOException, InterruptedException; 72 | 73 | /** 74 | * Checks whether the task has finished. 75 | * @param workspace the workspace in use 76 | * @param launcher a way to start processes (currently unused) 77 | * @param logger a way to report special messages 78 | * @return an exit code (zero is successful), or null if the task appears to still be running 79 | */ 80 | public @CheckForNull Integer exitStatus(FilePath workspace, Launcher launcher, TaskListener logger) throws IOException, InterruptedException { 81 | if (Util.isOverridden(Controller.class, getClass(), "exitStatus", FilePath.class, Launcher.class)) { 82 | return exitStatus(workspace, launcher); 83 | } else if (Util.isOverridden(Controller.class, getClass(), "exitStatus", FilePath.class)) { 84 | return exitStatus(workspace); 85 | } else { 86 | throw new AbstractMethodError("implement exitStatus(FilePath, Launcher, TaskListener)"); 87 | } 88 | } 89 | 90 | /** @deprecated use {@link #exitStatus(FilePath, Launcher, TaskListener)} instead */ 91 | @Deprecated 92 | public @CheckForNull Integer exitStatus(FilePath workspace, Launcher launcher) throws IOException, InterruptedException { 93 | return exitStatus(workspace, launcher, TaskListener.NULL); 94 | } 95 | 96 | /** @deprecated use {@link #exitStatus(FilePath, Launcher, TaskListener)} instead */ 97 | @Deprecated 98 | public @CheckForNull Integer exitStatus(FilePath workspace) throws IOException, InterruptedException { 99 | return exitStatus(workspace, createLauncher(workspace)); 100 | } 101 | 102 | /** 103 | * Obtain the process output. 104 | * Intended for use after {@link #exitStatus(FilePath, Launcher)} has returned a non-null status. 105 | * The result is undefined if {@link DurableTask#captureOutput} was not called before launch; generally an {@link IOException} will result. 106 | * @param workspace the workspace in use 107 | * @param launcher a way to start processes (currently unused) 108 | * @return the output of the process as raw bytes (may be empty but not null) 109 | * @see DurableTask#charset 110 | * @see DurableTask#defaultCharset 111 | */ 112 | public @NonNull byte[] getOutput(@NonNull FilePath workspace, @NonNull Launcher launcher) throws IOException, InterruptedException { 113 | throw new IOException("Did not implement getOutput in " + getClass().getName()); 114 | } 115 | 116 | /** 117 | * Tries to stop any running task. 118 | * @param workspace the workspace in use 119 | * @param launcher a way to start processes 120 | */ 121 | public void stop(FilePath workspace, Launcher launcher) throws IOException, InterruptedException { 122 | if (Util.isOverridden(Controller.class, getClass(), "stop", FilePath.class)) { 123 | stop(workspace); 124 | } else { 125 | throw new AbstractMethodError("implement stop(FilePath, Launcher)"); 126 | } 127 | } 128 | 129 | /** @deprecated use {@link #stop(FilePath, Launcher)} instead */ 130 | public void stop(FilePath workspace) throws IOException, InterruptedException { 131 | stop(workspace, createLauncher(workspace)); 132 | } 133 | 134 | private static Launcher createLauncher(FilePath workspace) throws IOException, InterruptedException { 135 | return workspace.createLauncher(new LogTaskListener(Logger.getLogger(Controller.class.getName()), Level.FINE)); 136 | } 137 | 138 | /** 139 | * Cleans up after a task is done. 140 | * Should delete any temporary files created by {@link DurableTask#launch}. 141 | * @param workspace the workspace in use 142 | */ 143 | public abstract void cleanup(FilePath workspace) throws IOException, InterruptedException; 144 | 145 | /** 146 | * Should be overridden to provide specific information about the status of an external process, for diagnostic purposes. 147 | * @return {@link #toString} by default 148 | */ 149 | public String getDiagnostics(FilePath workspace, Launcher launcher) throws IOException, InterruptedException { 150 | return toString(); 151 | } 152 | 153 | private static final long serialVersionUID = 1L; 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/durabletask/DurableTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2014 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.durabletask; 26 | 27 | import hudson.EnvVars; 28 | import hudson.ExtensionPoint; 29 | import hudson.FilePath; 30 | import hudson.Launcher; 31 | import hudson.model.AbstractDescribableImpl; 32 | import hudson.model.TaskListener; 33 | import java.io.IOException; 34 | import java.nio.charset.Charset; 35 | import java.util.logging.Level; 36 | import java.util.logging.Logger; 37 | import edu.umd.cs.findbugs.annotations.NonNull; 38 | 39 | /** 40 | * A task which may be run asynchronously on a build node and withstand disconnection of the slave agent. 41 | * Should have a descriptor, and a {@code config.jelly} for form data binding. 42 | */ 43 | public abstract class DurableTask extends AbstractDescribableImpl implements ExtensionPoint { 44 | 45 | private static final Logger LOGGER = Logger.getLogger(DurableTask.class.getName()); 46 | 47 | @Override public DurableTaskDescriptor getDescriptor() { 48 | return (DurableTaskDescriptor) super.getDescriptor(); 49 | } 50 | 51 | /** 52 | * Launches a durable task. 53 | * @param env basic environment variables to use during launch 54 | * @param workspace the workspace to use 55 | * @param launcher a way to start processes 56 | * @param listener log output for the build 57 | * @return a way to check up on the task’s subsequent status 58 | */ 59 | public abstract Controller launch(EnvVars env, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException; 60 | 61 | /** 62 | * Requests that standard output of the task be captured rather than streamed. 63 | * If you use {@link Controller#watch}, standard output will not be sent to {@link Handler#output}; it will be included in {@link Handler#exited} instead. 64 | * Otherwise (using polling mode), standard output will not be sent to {@link Controller#writeLog}; call {@link Controller#getOutput} to collect. 65 | * Standard error should still be streamed to the log. 66 | * Should be called prior to {@link #launch} to take effect. 67 | * @throws UnsupportedOperationException if this implementation does not support that mode 68 | */ 69 | public void captureOutput() throws UnsupportedOperationException { 70 | throw new UnsupportedOperationException("Capturing of output is not implemented in " + getClass().getName()); 71 | } 72 | 73 | /** 74 | * Requests that a specified charset be used to transcode process output. 75 | * The encoding of {@link Controller#writeLog} and {@link Controller#getOutput} is then presumed to be UTF-8. 76 | * If not called, no translation is performed. 77 | * @param cs the character set in which process output is expected to be 78 | */ 79 | public void charset(@NonNull Charset cs) { 80 | LOGGER.log(Level.WARNING, "The charset method should be overridden in {0}", getClass().getName()); 81 | } 82 | 83 | /** 84 | * Requests that the node’s system charset be used to transcode process output. 85 | * The encoding of {@link Controller#writeLog} and {@link Controller#getOutput} is then presumed to be UTF-8. 86 | * If not called, no translation is performed. 87 | */ 88 | public void defaultCharset() { 89 | LOGGER.log(Level.WARNING, "The defaultCharset method should be overridden in {0}", getClass().getName()); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/durabletask/DurableTaskDescriptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2014 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.durabletask; 26 | 27 | import hudson.model.Descriptor; 28 | 29 | /** 30 | * Descriptor type for {@link DurableTask}. 31 | */ 32 | public abstract class DurableTaskDescriptor extends Descriptor {} 33 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/durabletask/Handler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2016 CloudBees, Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.durabletask; 26 | 27 | import hudson.FilePath; 28 | import hudson.Launcher; 29 | import hudson.remoting.VirtualChannel; 30 | import java.io.InputStream; 31 | import java.io.Serializable; 32 | import edu.umd.cs.findbugs.annotations.NonNull; 33 | import edu.umd.cs.findbugs.annotations.Nullable; 34 | import hudson.remoting.Asynchronous; 35 | import org.jenkinsci.remoting.SerializableOnlyOverRemoting; 36 | 37 | /** 38 | * A remote handler which may be sent to an agent and handle process output and results. 39 | * If it needs to communicate with the master, you may use {@link VirtualChannel#export}. 40 | * @see Controller#watch 41 | */ 42 | public abstract class Handler implements SerializableOnlyOverRemoting { 43 | 44 | /** 45 | * Notification that new process output is available. 46 | *

Should only be called when at least one byte is available. 47 | * Whatever bytes are actually read will not be offered on the next call, if there is one; there is no need to close the stream. 48 | *

There is no guarantee that output is offered in the form of complete lines of text, 49 | * though in the typical case of line-oriented output it is likely that it will end in a newline. 50 | *

Buffering is the responsibility of the caller, and {@link InputStream#markSupported} may be false. 51 | * @param stream a way to read process output which has not already been handled 52 | * @throws Exception if anything goes wrong, this watch is deactivated 53 | */ 54 | public abstract void output(@NonNull InputStream stream) throws Exception; 55 | 56 | /** 57 | * Notification that the process has exited or vanished. 58 | * {@link #output} should have been called with any final uncollected output. 59 | *

Any metadata associated with the process may be deleted after this call completes, rendering subsequent {@link Controller} calls unsatisfiable. 60 | *

Note that unlike {@link Controller#exitStatus(FilePath, Launcher)}, no specialized {@link Launcher} is available on the agent, 61 | * so if there are specialized techniques for determining process liveness they will not be considered here; 62 | * you still need to occasionally poll for an exit status from the master. 63 | * @param code the exit code, if known (0 conventionally represents success); may be negative for anomalous conditions such as a missing process 64 | * @param output standard output captured, if {@link DurableTask#captureOutput} was called; else null 65 | */ 66 | @Asynchronous 67 | public abstract void exited(int code, @Nullable byte[] output) throws Exception; 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/plugins/durabletask/PowershellScript.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License 3 | * 4 | * Copyright 2017 Gabriel Loewen 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package org.jenkinsci.plugins.durabletask; 26 | 27 | import edu.umd.cs.findbugs.annotations.NonNull; 28 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 29 | import hudson.*; 30 | import hudson.util.ListBoxModel; 31 | import hudson.util.ListBoxModel.Option; 32 | 33 | import java.io.InputStream; 34 | import java.util.ArrayList; 35 | import java.util.Arrays; 36 | import java.util.List; 37 | import hudson.model.TaskListener; 38 | import java.io.IOException; 39 | 40 | import jenkins.model.Jenkins; 41 | import org.apache.commons.lang.StringUtils; 42 | import org.kohsuke.accmod.Restricted; 43 | import org.kohsuke.accmod.restrictions.NoExternalUse; 44 | import org.kohsuke.stapler.DataBoundConstructor; 45 | import java.io.OutputStream; 46 | import java.nio.charset.Charset; 47 | import java.util.logging.Level; 48 | import java.util.logging.Logger; 49 | 50 | import org.kohsuke.stapler.DataBoundSetter; 51 | 52 | import edu.umd.cs.findbugs.annotations.NonNull; 53 | 54 | /** 55 | * Runs a Powershell script 56 | */ 57 | public final class PowershellScript extends FileMonitoringTask { 58 | @SuppressFBWarnings("MS_SHOULD_BE_FINAL") // Used to control usage of binary or shell wrapper 59 | @Restricted(NoExternalUse.class) 60 | public static boolean USE_BINARY_WRAPPER = Boolean.getBoolean(PowershellScript.class.getName() + ".USE_BINARY_WRAPPER"); 61 | 62 | private final String script; 63 | private String powershellBinary = "powershell"; 64 | private boolean usesBom = true; 65 | private boolean loadProfile; 66 | private boolean capturingOutput; 67 | private static final Logger LOGGER = Logger.getLogger(PowershellScript.class.getName()); 68 | private static final String LAUNCH_DIAGNOSTICS_PROP = PowershellScript.class.getName() + ".LAUNCH_DIAGNOSTICS"; 69 | 70 | /** 71 | * Enables the debug flag for the binary wrapper. 72 | */ 73 | @SuppressWarnings("FieldMayBeFinal") 74 | // TODO use SystemProperties if and when unrestricted 75 | private static boolean LAUNCH_DIAGNOSTICS = Boolean.getBoolean(LAUNCH_DIAGNOSTICS_PROP); 76 | 77 | @DataBoundConstructor public PowershellScript(String script) { 78 | this.script = script; 79 | } 80 | 81 | public String getPowershellBinary() { 82 | return powershellBinary; 83 | } 84 | 85 | @DataBoundSetter 86 | public void setPowershellBinary(String powershellBinary) { 87 | this.powershellBinary = powershellBinary; 88 | } 89 | 90 | public boolean isLoadProfile() { 91 | return loadProfile; 92 | } 93 | 94 | @DataBoundSetter 95 | public void setLoadProfile(boolean loadProfile) { 96 | this.loadProfile = loadProfile; 97 | } 98 | 99 | public String getScript() { 100 | return script; 101 | } 102 | 103 | @Override public void captureOutput() { 104 | capturingOutput = true; 105 | } 106 | 107 | @Override protected FileMonitoringController doLaunch(FilePath ws, Launcher launcher, TaskListener listener, EnvVars envVars) throws IOException, InterruptedException { 108 | 109 | FilePath nodeRoot = getNodeRoot(ws); 110 | AgentInfo agentInfo = getAgentInfo(nodeRoot); 111 | PowershellController c = new PowershellController(ws, envVars.get(COOKIE)); 112 | 113 | List powershellArgs = new ArrayList<>(); 114 | if (!loadProfile) { 115 | powershellArgs.add("-NoProfile"); 116 | } 117 | powershellArgs.add("-NonInteractive"); 118 | if (!launcher.isUnix()) { 119 | powershellArgs.addAll(Arrays.asList("-ExecutionPolicy", "Bypass")); 120 | } 121 | 122 | if (launcher.isUnix() || "pwsh".equals(powershellBinary)) { 123 | usesBom = false; 124 | } 125 | 126 | List launcherCmd = null; 127 | FilePath binary; 128 | // Binary does not support pwsh on linux 129 | boolean pwshLinux; 130 | if ((agentInfo.getOs() == AgentInfo.OsType.LINUX) && "pwsh".equals(powershellBinary)) { 131 | pwshLinux = true; 132 | } else { 133 | pwshLinux = false; 134 | } 135 | if (USE_BINARY_WRAPPER && !pwshLinux && (binary = requestBinary(nodeRoot, agentInfo, ws, c)) != null) { 136 | launcherCmd = binaryLauncherCmd(c, ws, binary.getRemote(), c.getPowerShellScriptFile(ws).getRemote(), powershellArgs); 137 | if (!usesBom) { 138 | // There is no need to add a BOM with Open PowerShell / PowerShell Core 139 | c.getPowerShellScriptFile(ws).write(script, "UTF-8"); 140 | } else { 141 | // Write the Windows PowerShell scripts out with a UTF8 BOM 142 | writeWithBom(c.getPowerShellScriptFile(ws), script); 143 | } 144 | } 145 | 146 | if (launcherCmd == null) { 147 | launcherCmd = scriptLauncherCmd(c, ws, powershellArgs); 148 | 149 | String scriptWrapper = generateScriptWrapper(powershellBinary, powershellArgs, c.getPowerShellScriptFile(ws)); 150 | 151 | // Add an explicit exit to the end of the script so that exit codes are propagated 152 | String scriptWithExit = script + "\r\nexit $LASTEXITCODE"; 153 | 154 | // Copy the helper script from the resources directory into the workspace 155 | c.getPowerShellHelperFile(ws).copyFrom(getClass().getResource("powershellHelper.ps1")); 156 | 157 | if (!usesBom) { 158 | // There is no need to add a BOM with Open PowerShell / PowerShell Core 159 | c.getPowerShellScriptFile(ws).write(scriptWithExit, "UTF-8"); 160 | if (!capturingOutput) { 161 | c.getPowerShellWrapperFile(ws).write(scriptWrapper, "UTF-8"); 162 | } 163 | } else { 164 | // Write the Windows PowerShell scripts out with a UTF8 BOM 165 | writeWithBom(c.getPowerShellScriptFile(ws), scriptWithExit); 166 | if (!capturingOutput) { 167 | writeWithBom(c.getPowerShellWrapperFile(ws), scriptWrapper); 168 | } 169 | } 170 | 171 | } 172 | 173 | LOGGER.log(Level.FINE, "launching {0}", launcherCmd); 174 | Launcher.ProcStarter ps = launcher.launch().cmds(launcherCmd).envs(escape(envVars)).pwd(ws).quiet(true); 175 | ps.readStdout().readStderr(); // TODO see BourneShellScript 176 | Proc p = ps.start(); 177 | c.registerForCleanup(p.getStdout()); 178 | c.registerForCleanup(p.getStderr()); 179 | 180 | return c; 181 | } 182 | 183 | @NonNull 184 | private List binaryLauncherCmd(PowershellController c, FilePath ws, String binaryPath, String scriptPath, List powershellArgs) throws IOException, InterruptedException { 185 | String logFile = c.getLogFile(ws).getRemote(); 186 | String resultFile = c.getResultFile(ws).getRemote(); 187 | String outputFile = c.getOutputFile(ws).getRemote(); 188 | String controlDirPath = c.controlDir(ws).getRemote(); 189 | 190 | List cmd = new ArrayList<>(); 191 | cmd.add(binaryPath); 192 | cmd.add("-daemon"); 193 | cmd.add(String.format("-executable=%s", powershellBinary)); 194 | // Caution: the arguments must be separated by a comma AND a space to be parsed correctly 195 | cmd.add(String.format("-args=%s, -Command, %s", String.join(", ", powershellArgs), generateCommandWrapper(scriptPath, capturingOutput, outputFile, usesBom, c.getTemporaryOutputFile(ws).getRemote()))); 196 | cmd.add("-controldir=" + controlDirPath); 197 | cmd.add("-result=" + resultFile); 198 | cmd.add("-log=" + logFile); 199 | if (LAUNCH_DIAGNOSTICS) { 200 | cmd.add("-debug"); 201 | } 202 | return cmd; 203 | } 204 | 205 | private List scriptLauncherCmd(PowershellController c, FilePath ws, List powershellArgs) throws IOException, InterruptedException { 206 | List args = new ArrayList<>(); 207 | String cmd; 208 | if (capturingOutput) { 209 | cmd = String.format(". '%s'; Execute-AndWriteOutput -MainScript '%s' -OutputFile '%s' -LogFile '%s' -ResultFile '%s' -CaptureOutput;", 210 | quote(c.getPowerShellHelperFile(ws)), 211 | quote(c.getPowerShellScriptFile(ws)), 212 | quote(c.getOutputFile(ws)), 213 | quote(c.getLogFile(ws)), 214 | quote(c.getResultFile(ws))); 215 | } else { 216 | cmd = String.format(". '%s'; Execute-AndWriteOutput -MainScript '%s' -LogFile '%s' -ResultFile '%s';", 217 | quote(c.getPowerShellHelperFile(ws)), 218 | quote(c.getPowerShellWrapperFile(ws)), 219 | quote(c.getLogFile(ws)), 220 | quote(c.getResultFile(ws))); 221 | } 222 | 223 | args.add(powershellBinary); 224 | args.addAll(powershellArgs); 225 | args.addAll(Arrays.asList("-Command", cmd)); 226 | 227 | return args; 228 | } 229 | 230 | /** 231 | * Fix https://issues.jenkins.io/browse/JENKINS-59529 232 | * Fix https://issues.jenkins.io/browse/JENKINS-65597 233 | * Wrap invocation of powershellScript.ps1 in a try/catch in order to propagate PowerShell errors like: 234 | * command/script not recognized, parameter not found, parameter validation failed, parse errors, etc. In 235 | * PowerShell, $LASTEXITCODE applies **only** to the invocation of native apps and is not set when built-in 236 | * PowerShell commands or script invocation fails. 237 | * While you **could** prepend your script with "$ErrorActionPreference = 'Stop';