diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..17935bd2a35a9bf74c8634ddf4924117647f5bc8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,32 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org +# +# Subline Text: https://github.com/editorconfig/editorconfig-sublime +# Emacs: https://github.com/editorconfig/editorconfig-emacs +# Jetbrains (IntelliJ etc): https://github.com/editorconfig/editorconfig-jetbrains +# Eclipse: ?? +# + + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[**.jelly] +indent_style = space +indent_size = 2 + +[**.js] +indent_style = space +indent_size = 4 + +[**.css] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false~ \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index ceccd3d6453a2bd1a1c2528399fd0ac2145fd88d..0000000000000000000000000000000000000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 8b72c68c7d545c50085188dac55d278550d80e5a..0000000000000000000000000000000000000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/.idea/groovyc.xml b/.idea/groovyc.xml deleted file mode 100644 index c24d9163c14da67bdad46142120be95c431a8c57..0000000000000000000000000000000000000000 --- a/.idea/groovyc.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/BUILDING.TXT b/BUILDING.TXT index 9620b1b88b9f31a8b32cc37c2da0316d5c639d25..09766de355e22b3f2469dc88cc0a1ba35d8aa19c 100644 --- a/BUILDING.TXT +++ b/BUILDING.TXT @@ -1,6 +1,12 @@ -If you want simply to have the jenkins.war as fast as possible (without test execution), just use : -mvn clean install -pl war -am -DskipTests -the war will be in war/target/jenkins.war (you can play with it) +If you want simply to have the jenkins.war as fast as possible (without test +execution), run: + + mvn clean install -pl war -am -DskipTests + +The WAR file will be in war/target/jenkins.war (you can play with it) You can deactivate test-harness execution with -Dskip-test-harness -Have Fun !! +For more information on building Jenkins, visit +https://wiki.jenkins-ci.org/display/JENKINS/Building+Jenkins + +Have Fun !! diff --git a/README.md b/README.md index 8737be1df017d74497cea7595df898d0a59aece1..fa8bad71fefaf8fffbffddea955e68e7c8b1721f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Copyright © 2004–, Kohsuke Kawaguchi, Sun Microsystems, Inc., and a number of About ----- -In a nutshell Jenkins CI is the leading open-source continuous integration server. Built with Java, it provides over 300 plugins to support building and testing virtually any project. +In a nutshell Jenkins CI is the leading open-source continuous integration server. Built with Java, it provides over 930 plugins to support building and testing virtually any project. Downloads --------- @@ -26,4 +26,3 @@ All about Jenkins CI can be found on our [website]. Follow us on Twitter @[jenki [GitHub]: https://github.com/jenkinsci/jenkins [website]: http://jenkins-ci.org [jenkinsci]: http://twitter.com/jenkinsci - diff --git a/changelog.html b/changelog.html index 30843bc035a2f9ecf48a6bf66158e04d253a5650..abbe633eca7f0b9b26cedd5688207e1ab66bbb93 100644 --- a/changelog.html +++ b/changelog.html @@ -55,5621 +55,1858 @@ Upcoming changes -

What's new in 1.533 (2013/09/29)

+

What's new in 1.572 (2014/07/13)

-

What's new in 1.532 (2013/09/23)

+

What's new in 1.571 (2014/07/07)

+

What's new in 1.570 (2014/06/29)

+ -

What's new in 1.531 (2013/09/16)

+

What's new in 1.569 (2014/06/23)

+

What's new in 1.568 (2014/06/15)

+ -

What's new in 1.530 (2013/09/09)

+

What's new in 1.567 (2014/06/09)

+

What's new in 1.566 (2014/06/01)

+ +

What's new in 1.565 (2014/05/26)

+ +

What's new in 1.564 (2014/05/19)

+ +

What's new in 1.563 (2014/05/11)

+ +

What's new in 1.562 (2014/05/03)

+ -

What's new in 1.529 (2013/08/26)

+

What's new in 1.561 (2014/04/27)

-

What's new in 1.528 (2013/08/18)

- -

What's new in 1.527 (2013/08/12)

- -

What's new in 1.526 (2013/08/05)

+

What's new in 1.560 (2014/04/20)

+

What's new in 1.559 (2014/04/13)

+ +

What's new in 1.558 (2014/04/06)

+ +

What's new in 1.557 (2014/03/31)

+ -

What's new in 1.525 (2013/07/29)

-

Same as 1.524; botched release.

-

What's new in 1.524 (2013/07/23)

+

What's new in 1.556 (2014/03/23)

+ +

What's new in 1.555 (2014/03/16)

-

What's new in 1.523 (2013/07/14)

+

What's new in 1.554 (2014/03/09)

-

What's new in 1.522 (2013/07/06)

+

What's new in 1.553 (2014/03/02)

-

What's new in 1.521 (2013/07/02)

+

What's new in 1.552 (2014/02/24)

+

What's new in 1.551 (2014/02/14)

+ -

What's new in 1.520 (2013/06/25)

- +

What's new in 1.550 (2014/02/09)

+ +

What's new in 1.549 (2014/01/25)

+ +

What's new in 1.548 (2014/01/20)

+ -

What's new in 1.519 (2013/06/17)

+

What's new in 1.547 (2014/01/12)

-

What's new in 1.518 (2013/06/11)

+

What's new in 1.546 (2014/01/06)

+

What's new in 1.545 (2013/12/31)

+ -

What's new in 1.517 (2013/06/02)

+

What's new in 1.544 (2013/12/15)

-

What's new in 1.516 (2013/05/27)

+

What's new in 1.543 (2013/12/10)

+

What's new in 1.542 (2013/12/02)

+ -

What's new in 1.515 (2013/05/18)

+

What's new in 1.541 (2013/11/24)

-

What's new in 1.514 (2013/05/01)

+

What's new in 1.540 (2013/11/17)

-

What's new in 1.513 (2013/04/28)

- -

What's new in 1.512 (2013/04/21)

- -

What's new in 1.511 (2013/04/14)

- -

What's new in 1.510 (2013/04/06)

- -

What's new in 1.509 (2013/04/02)

- -

What's new in 1.508 (2013/03/25)

- -

What's new in 1.507 (2013/03/24)

- -

What's new in 1.506 (2013/03/17)

- -

What's new in 1.505 (2013/03/10)

- -

What's new in 1.504 (2013/03/03)

- -

What's new in 1.503 (2013/02/26)

- -

What's new in 1.502 (2013/02/16)

- -

What's new in 1.501 (2013/02/10)

- -

What's new in 1.500 (2013/01/26)

- -

What's new in 1.499 (2013/01/13)

- -

What's new in 1.498 (2013/01/07)

- -

What's new in 1.497 (2013/01/06)

- -

What's new in 1.496 (2012/12/30)

- -

What's new in 1.495 (2012/12/24)

- -

What's new in 1.494 (2012/12/16)

- -

What's new in 1.493 (2012/12/09)

- -

What's new in 1.492 (2012/11/25)

- -

What's new in 1.491 (2012/11/18)

- -

What's new in 1.490 (2012/11/12)

- -

What's new in 1.489 (2012/11/04)

- -

What's new in 1.488 (2012/10/28)

- -

What's new in 1.487 (2012/10/23)

- -

What's new in 1.486 (2012/10/14)

- -

What's new in 1.485 (2012/10/07)

- -

What's new in 1.484 (2012/09/30)

- -

What's new in 1.483 (2012/09/23)

- -

What's new in 1.482 (2012/09/16)

- -

What's new in 1.481 (2012/09/09)

- -

What's new in 1.480 (2012/09/03)

- -

What's new in 1.479 (2012/08/29)

- -

What's new in 1.478 (2012/08/20)

- -

What's new in 1.477 (2012/08/08)

- -

What's new in 1.476 (2012/07/31)

- -

What's new in 1.475 (2012/07/22)

- -

What's new in 1.474 (2012/07/09)

- -

What's new in 1.473 (2012/07/01)

- -

What's new in 1.472 (2012/06/24)

- -

What's new in 1.471 (2012/06/18)

- -

What's new in 1.470 (2012/06/13)

- -

What's new in 1.469 (2012/06/11)

- -

What's new in 1.468 (2012/06/11)

- -

What's new in 1.467 (2012/06/04)

- -

What's new in 1.466 (2012/05/28)

- -

What's new in 1.465 (2012/05/21)

- -

What's new in 1.464 (2012/05/14)

- -

What's new in 1.463 (2012/05/07)

- -

What's new in 1.462 (2012/04/30)

- -

What's new in 1.461 (2012/04/23)

- -

What's new in 1.460 (2012/04/14)

- -

What's new in 1.459 (2012/04/09)

- -

What's new in 1.458 (2012/04/02)

- -

What's new in 1.457 (2012/03/26)

- -

What's new in 1.456 (2012/03/19)

- -

What's new in 1.455 (2012/03/12)

- -

What's new in 1.454 (2012/03/05)

- -

What's new in 1.453 (2012/03/05)

- -

What's new in 1.452 (2012/02/27)

- -

What's new in 1.451 (2012/02/13)

- -

What's new in 1.450 (2012/01/30)

- -

What's new in 1.449 (2012/01/23)

- -

What's new in 1.448 (2012/01/17)

- -

What's new in 1.447 (2012/01/09)

- -

What's new in 1.446 (2012/01/02)

- -

What's new in 1.445 (2011/12/26)

- -

What's new in 1.444 (2011/12/19)

- -

What's new in 1.443 (2011/12/12)

- -

What's new in 1.442 (2011/12/05)

- -

What's new in 1.441 (2011/11/27)

- -

What's new in 1.440 (2011/11/17)

- -

What's new in 1.439 (2011/11/14)

- -

What's new in 1.438 (2011/11/07)

- -

What's new in 1.437 (2011/10/31)

- -

What's new in 1.436 (2011/10/23)

- -

What's new in 1.435 (2011/10/17)

- -

What's new in 1.434 (2011/10/09)

- -

What's new in 1.433 (2011/10/01)

- -

What's new in 1.432 (2011/09/25)

- -

What's new in 1.431 (2011/09/19)

- -

What's new in 1.430 (2011/09/11)

- -

What's new in 1.429 (2011/09/06)

- -

What's new in 1.428 (2011/08/29)

- -

What's new in 1.427 (2011/08/19)

- -

What's new in 1.426 (2011/08/15)

- -

What's new in 1.425 (2011/08/08)

- -

What's new in 1.424 (2011/08/01)

- -

What's new in 1.423 (2011/07/25)

- -

What's new in 1.422 (2011/07/25)

- -

What's new in 1.421 (2011/07/17)

- -

What's new in 1.420 (2011/07/11)

- -

What's new in 1.419 (2011/07/05)

- -

What's new in 1.418 (2011/06/27)

- -

What's new in 1.417 (2011/06/20)

- -

What's new in 1.416 (2011/06/18)

- -

What's new in 1.415 (2011/06/12)

- -

What's new in 1.414 (2011/06/04)

- -

What's new in 1.413 (2011/05/22)

- -

What's new in 1.412 (2011/05/16)

- -

What's new in 1.411 (2011/05/09)

- -

What's new in 1.410 (2011/05/01)

- -

What's new in 1.409 (2011/04/25)

- -

What's new in 1.408 (2011/04/18)

- -

What's new in 1.407 (2011/04/15)

- -

What's new in 1.406 (2011/04/11)

- -

What's new in 1.405 (2011/04/04)

- -

What's new in 1.404 (2011/03/27)

- -

What's new in 1.403 (2011/03/20)

- -

What's new in 1.402 (2011/03/20)

- -

What's new in 1.401 (2011/03/13)

- -

What's new in 1.400 (2011/03/06)

- -

What's new in 1.399 (2011/02/27)

- -

What's new in 1.398 (2011/02/20)

- -

What's new in 1.397 (2011/02/12)

- -

What's new in 1.396 (2011/02/02)

- -

What's new in 1.395 (2011/01/21)

- -

What's new in 1.394 (2011/01/15)

- -

What's new in 1.393 (2011/01/09)

- -

What's new in 1.392 (2010/12/31)

- -

What's new in 1.391 (2010/12/26)

- -

What's new in 1.390 (2010/12/18)

- -

What's new in 1.389 (2010/12/11)

- -

What's new in 1.388 (2010/12/04)

- -

What's new in 1.387 (2010/11/27)

- -

What's new in 1.386 (2010/11/19)

- -

What's new in 1.385 (2010/11/15)

- -

What's new in 1.384 (2010/11/05)

- -

What's new in 1.383 (2010/10/29)

- -

What's new in 1.382 (2010/10/24)

- -

What's new in 1.381 (2010/10/16)

- -

What's new in 1.380 (2010/10/09)

- -

What's new in 1.379 (2010/10/02)

- -

What's new in 1.378 (2010/09/25)

- -

What's new in 1.377 (2010/09/19)

- -

What's new in 1.376 (2010/09/11)

- -

What's new in 1.375 (2010/09/07)

- -

What's new in 1.374 (2010/08/27)

- -

What's new in 1.373 (2010/08/23)

- -

What's new in 1.372 (2010/08/13)

- -

What's new in 1.371 (2010/08/09)

- -

What's new in 1.370 (2010/08/07)

- -

What's new in 1.369 (2010/07/30)

- -

What's new in 1.368 (2010/07/26)

- -

What's new in 1.367 (2010/07/16)

- -

What's new in 1.366 (2010/07/09)

- -

What's new in 1.365 (2010/07/05)

- -

What's new in 1.364 (2010/06/25)

- -

What's new in 1.363 (2010/06/18)

- -

What's new in 1.362 (2010/06/11)

- -

What's new in 1.361 (2010/06/04)

+

What's new in 1.539 (2013/11/11)

-

What's new in 1.360 (2010/05/28)

- -

What's new in 1.359 (2010/05/21)

+

What's new in 1.538 (2013/11/03)

-

What's new in 1.358 (2010/05/14)

- -

What's new in 1.357 (2010/05/07)

- -

What's new in 1.356 (2010/05/03)

- -

What's new in 1.355 (2010/04/16)

+

What's new in 1.537 (2013/10/27)

-

What's new in 1.354 (2010/04/12)

- -

What's new in 1.353 (2010/03/29)

+

What's new in 1.536 (2013/10/20)

-

What's new in 1.352 (2010/03/19)

+

What's new in 1.535 (2013/10/14)

-

What's new in 1.351 (2010/03/15)

+

What's new in 1.534 (2013/10/07)

-

What's new in 1.350 (2010/03/12)

- -

What's new in 1.349 (2010/03/05)

- -

What's new in 1.348 (2010/02/26)

+

What's new in 1.533 (2013/09/29)

-

What's new in 1.347 (2010/02/19)

+

What's new in 1.532 (2013/09/23)

-

What's new in 1.346 (2010/02/12)

+

What's new in 1.531 (2013/09/16)

-

What's new in 1.345 (2010/02/08)

- -

What's new in 1.344 (2010/02/05)

+

What's new in 1.530 (2013/09/09)

-

What's new in 1.343 (2010/01/29)

+

What's new in 1.529 (2013/08/26)

-

What's new in 1.342 (2010/01/22)

- -

What's new in 1.341 (2010/01/15)

+

What's new in 1.528 (2013/08/18)

-

What's new in 1.340 (2010/01/11)

- -

What's new in 1.339 (2009/12/24)

+

What's new in 1.527 (2013/08/12)

-

What's new in 1.338 (2009/12/18)

- -

What's new in 1.337 (2009/12/11)

+

What's new in 1.526 (2013/08/05)

-

What's new in 1.336 (2009/11/28)

- +

What's new in 1.525 (2013/07/29)

+

Same as 1.524; botched release.

+

What's new in 1.524 (2013/07/23)

+ -

What's new in 1.335 (2009/11/20)

+

What's new in 1.523 (2013/07/14)

+

What's new in 1.522 (2013/07/06)

+ -

What's new in 1.334 (2009/11/16)

+

What's new in 1.521 (2013/07/02)

-

What's new in 1.333 (2009/11/09)

+

What's new in 1.520 (2013/06/25)

-

What's new in 1.332 (2009/11/02)

+

What's new in 1.519 (2013/06/17)

-

What's new in 1.331 (2009/10/30)

+

What's new in 1.518 (2013/06/11)

-

What's new in 1.330 (2009/10/23)

+

What's new in 1.517 (2013/06/02)

-

What's new in 1.329 (2009/10/16)

- -

What's new in 1.328 (2009/10/09)

+

What's new in 1.516 (2013/05/27)

-

What's new in 1.327 (2009/10/02)

+

What's new in 1.515 (2013/05/18)

-

What's new in 1.326 (2009/09/28)

- -

What's new in 1.325 (2009/09/25)

- -

What's new in 1.324 (2009/09/18)

+

What's new in 1.514 (2013/05/01)

-

What's new in 1.323 (2009/09/04)

+

What's new in 1.513 (2013/04/28)

+

What's new in 1.512 (2013/04/21)

+ -

What's new in 1.322 (2009/08/28)

- -

What's new in 1.321 (2009/08/21)

- -

What's new in 1.320 (2009/08/14)

+

What's new in 1.511 (2013/04/14)

-

What's new in 1.319 (2009/08/08)

+

What's new in 1.510 (2013/04/06)

-

What's new in 1.318 (2009/07/31)

+

What's new in 1.509 (2013/04/02)

-

What's new in 1.317 (2009/07/24)

- -

What's new in 1.316 (2009/07/17)

+

What's new in 1.508 (2013/03/25)

+

What's new in 1.507 (2013/03/24)

+ +

What's new in 1.506 (2013/03/17)

+ -

What's new in 1.315 (2009/07/10)

+

What's new in 1.505 (2013/03/10)

-

What's new in 1.314 (2009/07/02)

+

What's new in 1.504 (2013/03/03)

-

What's new in 1.313 (2009/06/26)

- -

What's new in 1.312 (2009/06/23)

- -

What's new in 1.311 (2009/06/19)

+

What's new in 1.503 (2013/02/26)

-

What's new in 1.310 (2009/06/14)

- -

What's new in 1.309 (2009/05/31)

- -

What's new in 1.308 (2009/05/28)

+

What's new in 1.502 (2013/02/16)

-

What's new in 1.307 (2009/05/22)

+

What's new in 1.501 (2013/02/10)

-

What's new in 1.306 (2009/05/16)

+

What's new in 1.500 (2013/01/26)

-

What's new in 1.305 (2009/05/16)

- -

What's new in 1.304 (2009/05/08)

- -

What's new in 1.303 (2009/05/03)

+

What's new in 1.499 (2013/01/13)

-

What's new in 1.302 (2009/05/01)

+

What's new in 1.498 (2013/01/07)

-

What's new in 1.301 (2009/04/25)

+

What's new in 1.497 (2013/01/06)

diff --git a/cli/pom.xml b/cli/pom.xml index a5cbe8ccc93c4d1391f2be28d01ffbc72afa3056..f3a0e84999b0be23454770731e1a813573189737 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ pom org.jenkins-ci.main - 1.535-SNAPSHOT + 1.574-SNAPSHOT cli @@ -13,6 +13,16 @@ Jenkins CLI + + org.powermock + powermock-module-junit4 + test + + + org.powermock + powermock-api-mockito + test + commons-codec commons-codec diff --git a/cli/src/main/java/hudson/cli/CLI.java b/cli/src/main/java/hudson/cli/CLI.java index 30d5fbc4c985216486a1240b4dd54fd774681e6c..bf75736560e51a82e6b81ebda5369c5c43f8b020 100644 --- a/cli/src/main/java/hudson/cli/CLI.java +++ b/cli/src/main/java/hudson/cli/CLI.java @@ -23,16 +23,14 @@ */ package hudson.cli; -import com.trilead.ssh2.crypto.PEMDecoder; import hudson.cli.client.Messages; import hudson.remoting.Channel; import hudson.remoting.PingThread; import hudson.remoting.Pipe; import hudson.remoting.RemoteInputStream; import hudson.remoting.RemoteOutputStream; -import hudson.remoting.SocketInputStream; +import hudson.remoting.SocketChannelStream; import hudson.remoting.SocketOutputStream; -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -50,7 +48,6 @@ import java.io.Closeable; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -62,13 +59,10 @@ import java.net.Socket; import java.net.URL; import java.net.URLConnection; import java.security.GeneralSecurityException; -import java.security.KeyFactory; import java.security.KeyPair; import java.security.PublicKey; import java.security.SecureRandom; import java.security.Signature; -import java.security.spec.DSAPrivateKeySpec; -import java.security.spec.DSAPublicKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -79,8 +73,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; -import java.io.Console; - import static java.util.logging.Level.*; /** @@ -209,7 +201,7 @@ public class CLI { } else { s = new Socket(); s.connect(clip.endpoint,3000); - out = new SocketOutputStream(s); + out = SocketChannelStream.out(s); } closables.add(new Closeable() { @@ -218,7 +210,7 @@ public class CLI { } }); - Connection c = new Connection(new SocketInputStream(s),out); + Connection c = new Connection(SocketChannelStream.in(s),out); switch (clip.version) { case 1: @@ -262,7 +254,7 @@ public class CLI { /** * If the server advertises CLI endpoint, returns its location. */ - private CliPort getCliTcpPort(String url) throws IOException { + protected CliPort getCliTcpPort(String url) throws IOException { URL _url = new URL(url); if (_url.getHost()==null || _url.getHost().length()==0) { throw new IOException("Invalid URL: "+url); @@ -284,6 +276,10 @@ public class CLI { flushURLConnection(head); if (p1==null && p2==null) { + // we aren't finding headers we are expecting. Is this even running Jenkins? + if (head.getHeaderField("X-Hudson")==null && head.getHeaderField("X-Jenkins")==null) + throw new IOException("There's no Jenkins running at "+url); + throw new IOException("No X-Jenkins-CLI2-Port among " + head.getHeaderFields().keySet()); } @@ -307,10 +303,12 @@ public class CLI { } catch (IOException e) { try { InputStream es = ((HttpURLConnection)conn).getErrorStream(); - while (es.read(buf) >= 0) { - // Ignore + if (es!=null) { + while (es.read(buf) >= 0) { + // Ignore + } + es.close(); } - es.close(); } catch (IOException ex) { // Ignore } @@ -386,7 +384,7 @@ public class CLI { public static int _main(String[] _args) throws Exception { List args = Arrays.asList(_args); - List candidateKeys = new ArrayList(); + PrivateKeyProvider provider = new PrivateKeyProvider(); boolean sshAuthRequestedExplicitly = false; String httpProxy=null; @@ -426,17 +424,9 @@ public class CLI { printUsage(Messages.CLI_NoSuchFileExists(f)); return -1; } - KeyPair kp; - try { - kp = loadKey(f); - } catch (IOException e) { - //if the PEM file is encrypted, IOException is thrown - kp = tryEncryptedFile(f); - } catch (GeneralSecurityException e) { - throw new Exception("Failed to load key: "+f,e); - } - if(kp != null) - candidateKeys.add(kp); + + provider.readFrom(f); + args = args.subList(2,args.size()); sshAuthRequestedExplicitly = true; continue; @@ -457,8 +447,8 @@ public class CLI { if(args.isEmpty()) args = Arrays.asList("help"); // default to help - if (candidateKeys.isEmpty()) - addDefaultPrivateKeyLocations(candidateKeys); + if (!provider.hasKeys()) + provider.readFromDefaultLocations(); CLIConnectionFactory factory = new CLIConnectionFactory().url(url).httpsProxyTunnel(httpProxy); String userInfo = new URL(url).getUserInfo(); @@ -468,10 +458,10 @@ public class CLI { CLI cli = factory.connect(); try { - if (!candidateKeys.isEmpty()) { + if (provider.hasKeys()) { try { // TODO: server verification - cli.authenticate(candidateKeys); + cli.authenticate(provider.getKeys()); } catch (IllegalStateException e) { if (sshAuthRequestedExplicitly) { System.err.println("The server doesn't support public key authentication"); @@ -523,103 +513,22 @@ public class CLI { * Loads RSA/DSA private key in a PEM format into {@link KeyPair}. */ public static KeyPair loadKey(File f, String passwd) throws IOException, GeneralSecurityException { - return loadKey(readPemFile(f), passwd); + return PrivateKeyProvider.loadKey(f, passwd); } public static KeyPair loadKey(File f) throws IOException, GeneralSecurityException { - return loadKey(f, null); - } - - private static String readPemFile(File f) throws IOException{ - FileInputStream is = new FileInputStream(f); - try { - DataInputStream dis = new DataInputStream(is); - byte[] bytes = new byte[(int) f.length()]; - dis.readFully(bytes); - dis.close(); - return new String(bytes); - } finally { - is.close(); - } + return loadKey(f, null); } - + /** * Loads RSA/DSA private key in a PEM format into {@link KeyPair}. */ public static KeyPair loadKey(String pemString, String passwd) throws IOException, GeneralSecurityException { - Object key = PEMDecoder.decode(pemString.toCharArray(), passwd); - if (key instanceof com.trilead.ssh2.signature.RSAPrivateKey) { - com.trilead.ssh2.signature.RSAPrivateKey x = (com.trilead.ssh2.signature.RSAPrivateKey)key; -// System.out.println("ssh-rsa " + new String(Base64.encode(RSASHA1Verify.encodeSSHRSAPublicKey(x.getPublicKey())))); - - return x.toJCEKeyPair(); - } - if (key instanceof com.trilead.ssh2.signature.DSAPrivateKey) { - com.trilead.ssh2.signature.DSAPrivateKey x = (com.trilead.ssh2.signature.DSAPrivateKey)key; - KeyFactory kf = KeyFactory.getInstance("DSA"); -// System.out.println("ssh-dsa " + new String(Base64.encode(DSASHA1Verify.encodeSSHDSAPublicKey(x.getPublicKey())))); - - return new KeyPair( - kf.generatePublic(new DSAPublicKeySpec(x.getY(), x.getP(), x.getQ(), x.getG())), - kf.generatePrivate(new DSAPrivateKeySpec(x.getX(), x.getP(), x.getQ(), x.getG()))); - } - - throw new UnsupportedOperationException("Unrecognizable key format: "+key); + return PrivateKeyProvider.loadKey(pemString, passwd); } public static KeyPair loadKey(String pemString) throws IOException, GeneralSecurityException { - return loadKey(pemString, null); - } - - private static KeyPair tryEncryptedFile(File f) throws IOException, GeneralSecurityException{ - KeyPair kp = null; - if(isPemEncrypted(f)){ - String passwd = askForPasswd(f.getCanonicalPath()); - kp = loadKey(f,passwd); - } - return kp; - } - - private static boolean isPemEncrypted(File f) throws IOException{ - String pemString = readPemFile(f); - //simple check if the file is encrypted - return pemString.contains("4,ENCRYPTED"); - } - - @SuppressWarnings("Since15") - @IgnoreJRERequirement - private static String askForPasswd(String filePath){ - try { - Console cons = System.console(); - String passwd = null; - if (cons != null){ - char[] p = cons.readPassword("%s", "Enter passphrase for "+filePath+":"); - passwd = String.valueOf(p); - } - return passwd; - } catch (LinkageError e) { - throw new Error("Your private key is encrypted, but we need Java6 to ask you password safely",e); - } - } - - /** - * try all the default key locations - */ - private static void addDefaultPrivateKeyLocations(List keyFileCandidates) { - File home = new File(System.getProperty("user.home")); - for (String path : new String[]{".ssh/id_rsa",".ssh/id_dsa",".ssh/identity"}) { - File key = new File(home,path); - if (key.exists()) { - try { - keyFileCandidates.add(loadKey(key)); - } catch (IOException e) { - // don't report an error. the user can still see it by using the -i option - LOGGER.log(FINE, "Failed to load "+key,e); - } catch (GeneralSecurityException e) { - LOGGER.log(FINE, "Failed to load " + key, e); - } - } - } + return loadKey(pemString, null); } /** diff --git a/cli/src/main/java/hudson/cli/Connection.java b/cli/src/main/java/hudson/cli/Connection.java index 232970869d95ae3b3206c60cfb3859eb5fd99a87..165a6deb7e6af83adf0ccda8cbe86a474414a65e 100644 --- a/cli/src/main/java/hudson/cli/Connection.java +++ b/cli/src/main/java/hudson/cli/Connection.java @@ -23,8 +23,7 @@ */ package hudson.cli; -import hudson.remoting.SocketInputStream; -import hudson.remoting.SocketOutputStream; +import hudson.remoting.SocketChannelStream; import org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher; @@ -35,7 +34,6 @@ import javax.crypto.SecretKey; import javax.crypto.interfaces.DHPublicKey; import javax.crypto.spec.DHParameterSpec; import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; @@ -64,7 +62,7 @@ public class Connection { public final DataOutputStream dout; public Connection(Socket socket) throws IOException { - this(new SocketInputStream(socket),new SocketOutputStream(socket)); + this(SocketChannelStream.in(socket),SocketChannelStream.out(socket)); } public Connection(InputStream in, OutputStream out) { @@ -128,7 +126,11 @@ public class Connection { } public byte[] readByteArray() throws IOException { - byte[] buf = new byte[din.readInt()]; + int bufSize = din.readInt(); + if (bufSize < 0) { + throw new IOException("DataInputStream unexpectedly returned negative integer"); + } + byte[] buf = new byte[bufSize]; din.readFully(buf); return buf; } diff --git a/cli/src/main/java/hudson/cli/PrivateKeyProvider.java b/cli/src/main/java/hudson/cli/PrivateKeyProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..834bb7234250d7d7bac2bd424aba5aa670be0fda --- /dev/null +++ b/cli/src/main/java/hudson/cli/PrivateKeyProvider.java @@ -0,0 +1,162 @@ +/* + * The MIT License + * + * Copyright (c) 2014 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import static java.util.logging.Level.FINE; + +import java.io.Console; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +import com.trilead.ssh2.crypto.PEMDecoder; + +/** + * Read DSA or RSA key from file(s) asking for password interactively. + * + * @author ogondza + * @since 1.556 + */ +public class PrivateKeyProvider { + + private List privateKeys = new ArrayList(); + + /** + * Get keys read so far. + * + * @return Possibly empty list. Never null. + */ + public List getKeys() { + return Collections.unmodifiableList(privateKeys); + } + + public boolean hasKeys() { + return !privateKeys.isEmpty(); + } + + /** + * Read keys from default keyFiles + * + * .ssh/id_rsa, .ssh/id_dsa and .ssh/identity. + * + * @return true if some key was read successfully. + */ + public boolean readFromDefaultLocations() { + final File home = new File(System.getProperty("user.home")); + + boolean read = false; + for (String path : new String[] {".ssh/id_rsa", ".ssh/id_dsa", ".ssh/identity"}) { + final File key = new File(home, path); + if (!key.exists()) continue; + + try { + + readFrom(key); + read = true; + } catch (IOException e) { + + LOGGER.log(FINE, "Failed to load " + key, e); + } catch (GeneralSecurityException e) { + + LOGGER.log(FINE, "Failed to load " + key, e); + } + } + + return read; + } + + /** + * Read key from keyFile. + */ + public void readFrom(File keyFile) throws IOException, GeneralSecurityException { + final String password = isPemEncrypted(keyFile) + ? askForPasswd(keyFile.getCanonicalPath()) + : null + ; + privateKeys.add(loadKey(keyFile, password)); + } + + private static boolean isPemEncrypted(File f) throws IOException{ + //simple check if the file is encrypted + return readPemFile(f).contains("4,ENCRYPTED"); + } + + private static String askForPasswd(String filePath){ + Console cons = System.console(); + String passwd = null; + if (cons != null){ + char[] p = cons.readPassword("%s", "Enter passphrase for " + filePath + ":"); + passwd = String.valueOf(p); + } + return passwd; + } + + public static KeyPair loadKey(File f, String passwd) throws IOException, GeneralSecurityException { + return loadKey(readPemFile(f), passwd); + } + + private static String readPemFile(File f) throws IOException{ + FileInputStream is = new FileInputStream(f); + try { + DataInputStream dis = new DataInputStream(is); + byte[] bytes = new byte[(int) f.length()]; + dis.readFully(bytes); + dis.close(); + return new String(bytes); + } finally { + is.close(); + } + } + + public static KeyPair loadKey(String pemString, String passwd) throws IOException, GeneralSecurityException { + Object key = PEMDecoder.decode(pemString.toCharArray(), passwd); + if (key instanceof com.trilead.ssh2.signature.RSAPrivateKey) { + com.trilead.ssh2.signature.RSAPrivateKey x = (com.trilead.ssh2.signature.RSAPrivateKey)key; + + return x.toJCEKeyPair(); + } + if (key instanceof com.trilead.ssh2.signature.DSAPrivateKey) { + com.trilead.ssh2.signature.DSAPrivateKey x = (com.trilead.ssh2.signature.DSAPrivateKey)key; + KeyFactory kf = KeyFactory.getInstance("DSA"); + + return new KeyPair( + kf.generatePublic(new DSAPublicKeySpec(x.getY(), x.getP(), x.getQ(), x.getG())), + kf.generatePrivate(new DSAPrivateKeySpec(x.getX(), x.getP(), x.getQ(), x.getG()))); + } + + throw new UnsupportedOperationException("Unrecognizable key format: " + key); + } + + private static final Logger LOGGER = Logger.getLogger(PrivateKeyProvider.class.getName()); +} diff --git a/cli/src/main/resources/hudson/cli/client/Messages_pt_BR.properties b/cli/src/main/resources/hudson/cli/client/Messages_pt_BR.properties index 1131ec8d17d04bacd323a99eaa3669896088d60a..106ba7ce84decb70f0b7332eb3a0777a861905e3 100644 --- a/cli/src/main/resources/hudson/cli/client/Messages_pt_BR.properties +++ b/cli/src/main/resources/hudson/cli/client/Messages_pt_BR.properties @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2004-2010, Sun Microsystems, Inc., Reginaldo L. Russinholi, Cleiber Silva +# Copyright (c) 2004-2010, Sun Microsystems, Inc., Reginaldo L. Russinholi, Cleiber Silva, Fernando Boaglio # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -21,21 +21,21 @@ # THE SOFTWARE. # Version mismatch. This CLI cannot work with this Hudson server -CLI.VersionMismatch=A vers\ufffdo n\ufffdo coincide. Esta CLI n\ufffdo pode funcionar com este servidor Hudson -# Hudson CLI\n\ -# Usage: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\ -# Options:\n\ +CLI.VersionMismatch=A vers\u00e3o n\u00e3o coincide. Esta CLI n\u00e3o pode funcionar com este servidor Hudson +# Hudson CLI\n\ +# Usage: java -jar jenkins-cli.jar [-s URL] command [opts...] args...\n\ +# Options:\n\ # \ -s URL : specify the server URL (defaults to the JENKINS_URL env var)\n\ -# \n\ -# The available commands depend on the server. Run the 'help' command to\n\ +# \n\ +# The available commands depend on the server. Run the 'help' command to\n\ # see the list. CLI.Usage=Jenkins CLI\n\ - Uso: java -jar jenkins-cli.jar [-s URL] comando [op\ufffd\ufffdes...] par\ufffdmetros...\n\ - Op\ufffd\ufffdes:\n\ - \ -s URL : a URL do servidor (por padr\ufffdo a vari\ufffdvel de ambiente JENKINS_URL \ufffd usada)\n\ + Uso: java -jar jenkins-cli.jar [-s URL] comando [op\u00e7\u00f5es...] par\u00e2metros...\n\ + Op\u00e7\u00f5es:\n\ + \ -s URL : a URL do servidor (por padr\u00e3o a vari\u00e1vel de ambiente JENKINS_URL \u00e9 usada)\n\ \n\ - Os comandos dispon\ufffdveis dependem do servidor. Execute o comando 'help' para\n\ + Os comandos dispon\u00edveis dependem do servidor. Execute o comando 'help' para\n\ ver a lista. # Neither -s nor the JENKINS_URL env var is specified. -CLI.NoURL=N\ufffdo foi especificado nem '-s' e nem a vari\ufffdvel de ambiente JENKINS_URL +CLI.NoURL=N\u00e3o foi especificado nem '-s' e nem a vari\u00e1vel de ambiente JENKINS_URL diff --git a/cli/src/test/java/hudson/cli/ConnectionMockTest.java b/cli/src/test/java/hudson/cli/ConnectionMockTest.java new file mode 100644 index 0000000000000000000000000000000000000000..77b319a4d249ee6856bdc60b66e5bd217c3829af --- /dev/null +++ b/cli/src/test/java/hudson/cli/ConnectionMockTest.java @@ -0,0 +1,56 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Ericsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import hudson.remoting.FastPipedInputStream; +import hudson.remoting.FastPipedOutputStream; + +import java.io.DataInputStream; +import java.io.IOException; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.powermock.api.mockito.PowerMockito.*; + +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +/** + * @author marco.miller@ericsson.com + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({Connection.class}) +public class ConnectionMockTest { + + @Test + public void shouldTolerateEmptyByteArrayUponStreamZeroValue() throws IOException { + DataInputStream din = mock(DataInputStream.class); + //when(din.readInt()).thenReturn(0) does not work; mock always return 0 for some TBD reason + Connection c = new Connection(din, new FastPipedOutputStream(new FastPipedInputStream())); + assertTrue(c.readByteArray().length == 0); + } +} diff --git a/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..376b1fa815f3f62988f741c93a63f2f2d090563a --- /dev/null +++ b/cli/src/test/java/hudson/cli/PrivateKeyProviderTest.java @@ -0,0 +1,140 @@ +/* + * The MIT License + * + * Copyright (c) 2014 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.doReturn; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyPair; +import java.util.Arrays; + +import org.hamcrest.Description; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; +import org.mockito.Mockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(CLI.class) // When mocking new operator caller has to be @PreparedForTest, not class itself +public class PrivateKeyProviderTest { + + @Test + public void specifyKeysExplicitly() throws Exception { + final CLI cli = fakeCLI(); + + final File dsaKey = keyFile(".ssh/id_dsa"); + final File rsaKey = keyFile(".ssh/id_rsa"); + + run("-i", dsaKey.getAbsolutePath(), "-i", rsaKey.getAbsolutePath(), "-s", "http://example.com"); + + verify(cli).authenticate(withKeyPairs( + keyPair(dsaKey), + keyPair(rsaKey) + )); + } + + @Test + public void useDefaultKeyLocations() throws Exception { + final CLI cli = fakeCLI(); + + final File rsaKey = keyFile(".ssh/id_rsa"); + final File dsaKey = keyFile(".ssh/id_dsa"); + + fakeHome(); + run("-s", "http://example.com"); + + verify(cli).authenticate(withKeyPairs( + keyPair(rsaKey), + keyPair(dsaKey) + )); + } + + private CLI fakeCLI() throws Exception { + final CLI cli = mock(CLI.class); + + final CLIConnectionFactory factory = mock(CLIConnectionFactory.class, Mockito.CALLS_REAL_METHODS); + factory.jenkins = new URL("http://example.com"); + doReturn(cli).when(factory).connect(); + + mockStatic(CLIConnectionFactory.class); + whenNew(CLIConnectionFactory.class).withNoArguments().thenReturn(factory); + + return cli; + } + + private void fakeHome() throws URISyntaxException { + final File home = new File(this.getClass().getResource(".ssh").toURI()).getParentFile(); + System.setProperty("user.home", home.getAbsolutePath()); + } + + private int run(String... args) throws Exception { + return CLI._main(args); + } + + private File keyFile(String name) throws URISyntaxException { + return new File(this.getClass().getResource(name).toURI()); + } + + private KeyPair keyPair(File file) throws IOException, GeneralSecurityException { + return PrivateKeyProvider.loadKey(file, null); + } + + private Iterable withKeyPairs(final KeyPair... expected) { + return Mockito.argThat(new ArgumentMatcher>() { + @Override public void describeTo(Description description) { + description.appendText(Arrays.asList(expected).toString()); + } + + @Override public boolean matches(Object argument) { + if (!(argument instanceof Iterable)) throw new IllegalArgumentException("Not an instance of Iterrable"); + + @SuppressWarnings("unchecked") + final Iterable actual = (Iterable) argument; + int i = 0; + for (KeyPair akp: actual) { + if (!eq(expected[i].getPublic(), akp.getPublic())) return false; + if (!eq(expected[i].getPrivate(), akp.getPrivate())) return false; + i++; + } + + return i == expected.length; + } + + private boolean eq(final Key expected, final Key actual) { + return Arrays.equals(expected.getEncoded(), actual.getEncoded()); + } + }); + } +} diff --git a/cli/src/test/resources/hudson/cli/.ssh/id_dsa b/cli/src/test/resources/hudson/cli/.ssh/id_dsa new file mode 100644 index 0000000000000000000000000000000000000000..be556daa6d02984101686a02939b6777c780db86 --- /dev/null +++ b/cli/src/test/resources/hudson/cli/.ssh/id_dsa @@ -0,0 +1,12 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIBugIBAAKBgQCA9mMzB1O52hpObIyaJXgFJQUmc1HV0NEJXsFFGh8U2l0Tkgv4 +fp3MWadiAMmc5H1ot4KQLXl7SwU7dHCCFcGcfQiOjeD5rWeZuHoPAJSDMilcJGE3 +Xo2C+wlescTByEgRRA16vdSlNaDJXKVxq9Wr59G8P4JC6/5EvpeypgYdTQIVAMTf +aC0O2EGLnJrNBsUdc1s+iUp9AoGAZA7pZYPMJHJWTanJb2DlWHn/QM63jfh38N6W +ERzmQQks6QdS7UkFlg9cbVGUtn0Yz2SfX3VKiMXNMkAdGD8loBcJS5w6oMMU7rcj +lldRQ63+fMgdVZYMF5bchC6RhQeGZQ8Imf2iFF28SsE4bi+K12HYgIO5bFxPFUTH +WSWsMLcCgYBgHJ90ZLU400axB5P0qw/0s4arPD0g53Vzi/Y2h5TJr3KPF2sEIbAc +2gpFEzUNY0hvH6REKJ+VPPUvlH6ieaXomW8pSGjv4SdxZhJRrDe+Ac/xQse1QdYx +uWJzpVm3cIGfqLxmQnrklnutI/1F62VZQlq9vjiZL7ir/00vdUTYHwIUUkttGGgl +a0rWLzPTPF4X4lZfFhk= +-----END DSA PRIVATE KEY----- diff --git a/cli/src/test/resources/hudson/cli/.ssh/id_dsa.pub b/cli/src/test/resources/hudson/cli/.ssh/id_dsa.pub new file mode 100644 index 0000000000000000000000000000000000000000..4a42a845727297abcfa1c1204d0a270392c5a9a6 --- /dev/null +++ b/cli/src/test/resources/hudson/cli/.ssh/id_dsa.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAID2YzMHU7naGk5sjJoleAUlBSZzUdXQ0QlewUUaHxTaXROSC/h+ncxZp2IAyZzkfWi3gpAteXtLBTt0cIIVwZx9CI6N4PmtZ5m4eg8AlIMyKVwkYTdejYL7CV6xxMHISBFEDXq91KU1oMlcpXGr1avn0bw/gkLr/kS+l7KmBh1NAAAAFQDE32gtDthBi5yazQbFHXNbPolKfQAAAIBkDullg8wkclZNqclvYOVYef9AzreN+Hfw3pYRHOZBCSzpB1LtSQWWD1xtUZS2fRjPZJ9fdUqIxc0yQB0YPyWgFwlLnDqgwxTutyOWV1FDrf58yB1VlgwXltyELpGFB4ZlDwiZ/aIUXbxKwThuL4rXYdiAg7lsXE8VRMdZJawwtwAAAIBgHJ90ZLU400axB5P0qw/0s4arPD0g53Vzi/Y2h5TJr3KPF2sEIbAc2gpFEzUNY0hvH6REKJ+VPPUvlH6ieaXomW8pSGjv4SdxZhJRrDe+Ac/xQse1QdYxuWJzpVm3cIGfqLxmQnrklnutI/1F62VZQlq9vjiZL7ir/00vdUTYHw== ogondza@localhost.localdomain diff --git a/cli/src/test/resources/hudson/cli/.ssh/id_rsa b/cli/src/test/resources/hudson/cli/.ssh/id_rsa new file mode 100644 index 0000000000000000000000000000000000000000..ee2ad6b569a3b5ebbea0d5d1bfcf6ed2bd0dbe27 --- /dev/null +++ b/cli/src/test/resources/hudson/cli/.ssh/id_rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAyTqwFqp5Ww2Tr/52D7hhdOwgzYGBUqxrOFopa+kjNEL1Yqwb ++mApUWZ+D3zN9PurhUcVUfeYVXiYWFJ0kG72HIJawL/0BR5oYxRJfumK8Z/sAzAL +xdhc5O5twETrr9gU3cxtvF5oJNP0I9HickAOeC+ZNpiDIIblrhvxXl/QwqrR+/Gv +Nb8TApj+rxXEfNp+N69iGnnxzWn1FeKeOAWpwoBAxZNoqBQAFacF7xfQnoygyekC +xk+ts2O5Zzv8iJ10sVf+x2Q79rxAtsc0xOGhZbBAzbmFTz0PE4iWuo/Vo1c6mM7u +/dam+FxB2NqPNw7W+4eiCnEVkiQZlrxmuGvK7wIDAQABAoIBACml1+QZDFzoBnUa +eVzvkFwesvtVnmp5/QcAwinvarXaVedCL9g2JtcOG3EhJ49YtzsyZxs7329xMja1 +eiKalJ157UaPc/XLQVegT0XRGEzCCJrwSr979F39awGsQgt28XqmYN/nui5FH/Z5 +7iAvWc9OKqu+DQWiZc8PQXmC4zYmvhGQ8vKx44RSqlWCjd9IqBVhpE5gxpI/SmCx +umUNNtoH0hBWr+MsVHzr6UUrC3a99+7bB4We8XMXXFLzbTUSgiYFmK+NxPs/Fux/ +IAyXAMbDw2HeqZ7g4kTaf4cvmVOwhh4zlvB4p7j301LdO1jmvs9z0fn/QJcTpVM7 +ISMKwAECgYEA/uKVdmOKTk3dKzKRFXtWJjqypOXakoX+25lUcVv2PXYRr8Sln9jC +A13fbhvwq+FqbdnNlB23ag5niCVLfUpB1DYYP5jd4lU8D6HZQiHlmokB6nLT9NIW +iTcG88E58Bta/l1Ue5Yn+LqluBC4i289wFbH1kZyxQ565s5dJEv9uAECgYEAyhwF +ZOqTK2lZe5uuN4owVLQaYFj9fsdFHULzlK/UAtkG1gCJhjBmwSEpZFFMH6WgwHk5 +SHJEom0uB4qRv8gQcxl9OSiDsp56ymr0NBhlPVXWr6IzLotLy5XBC1muqvYYlj7E +kHgSet/h8RUM/FeEiwOFHDU2DkMb8Qx1hfMdAu8CgYBSEsYL9CuB4WK5WTQMlcV8 +0+PYY0dJbSpOrgXZ5sHYsp8pWQn3+cUnbl/WxdpujkxGCR9AdX0tAmxmE5RGSNX/ +rleKiv/PtKB9bCFYQS/83ecnBkioCcpF7tknPm4YmcZoJ8dfcE94sSlRpti11WEu +AQOiRNcKCwqaLZMib/HIAQKBgQCdiOffeERMYypfgcJzAiCX9WZV0SeOCS7jFwub +ys17hsSgS/zl/pYpVXrY+dFXHZfGTvcKdB7xaB6nvCfND9lajfSgd+bndEYLvwAo +Fxfajizv64LvdZ4XytuUyEuwcHBLtBMs9Jqa8iU/8AOWMXVbkdvQV92RkleWNPrp +9MyZOwKBgQD9x8MnX5LVBfQKuL9qX6l9Da06EyMkzfz3obKn9AAJ3Xj9+45TNPJu +HnZyvJWesl1vDjXQTm+PVkdyE0WQgoiVX+wxno0hsoly5Uqb5EYHtTUrZzRpkyLK +1VmtDxT5D8gorUgn6crzk4PKaxRkPfAimZdlkQm6iOtuR3kqn5BtIQ== +-----END RSA PRIVATE KEY----- diff --git a/cli/src/test/resources/hudson/cli/.ssh/id_rsa.pub b/cli/src/test/resources/hudson/cli/.ssh/id_rsa.pub new file mode 100644 index 0000000000000000000000000000000000000000..91f8ff7180e5f05f989b9aec2104d102372e88a4 --- /dev/null +++ b/cli/src/test/resources/hudson/cli/.ssh/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDJOrAWqnlbDZOv/nYPuGF07CDNgYFSrGs4Wilr6SM0QvVirBv6YClRZn4PfM30+6uFRxVR95hVeJhYUnSQbvYcglrAv/QFHmhjFEl+6Yrxn+wDMAvF2Fzk7m3AROuv2BTdzG28Xmgk0/Qj0eJyQA54L5k2mIMghuWuG/FeX9DCqtH78a81vxMCmP6vFcR82n43r2IaefHNafUV4p44BanCgEDFk2ioFAAVpwXvF9CejKDJ6QLGT62zY7lnO/yInXSxV/7HZDv2vEC2xzTE4aFlsEDNuYVPPQ8TiJa6j9WjVzqYzu791qb4XEHY2o83Dtb7h6IKcRWSJBmWvGa4a8rv your_email@example.com diff --git a/core/pom.xml b/core/pom.xml index 1efb9691848e6bdd42272dccbafe0c95b4edbc4b..d2c68f4b9decfc86e63e53cc57a6e6e6ef297321 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 1.535-SNAPSHOT + 1.574-SNAPSHOT ../pom.xml @@ -42,7 +42,7 @@ THE SOFTWARE. true - 1.218 + 1.227 2.5.6.SEC03 1.8.9 @@ -112,7 +112,7 @@ THE SOFTWARE. org.jenkins-ci trilead-ssh2 - build214-jenkins-3 + build217-jenkins-5 org.kohsuke.stapler @@ -161,7 +161,7 @@ THE SOFTWARE. org.kohsuke.stapler stapler-adjunct-codemirror - 1.2 + 1.3 org.kohsuke.stapler @@ -174,7 +174,7 @@ THE SOFTWARE. com.infradna.tool bridge-method-annotation - 1.8 + 1.9 @@ -194,17 +194,17 @@ THE SOFTWARE. org.jenkins-ci annotation-indexer - 1.4 + 1.7 org.jenkins-ci bytecode-compatibility-transformer - 1.3 + 1.5 - org.jvnet.hudson + org.jenkins-ci task-reactor - 1.2 + 1.4 org.jvnet.localizer @@ -219,7 +219,17 @@ THE SOFTWARE. org.jvnet.hudson xstream - 1.4.4-jenkins-4 + 1.4.7-jenkins-1 + + + xmlpull + xmlpull + + + xpp3 + xpp3_min + + jfree @@ -469,12 +479,12 @@ THE SOFTWARE. org.jvnet.winp winp - 1.16 + 1.20 org.jenkins-ci memory-monitor - 1.7 + 1.8 org.codehaus.woodstox @@ -517,14 +527,9 @@ THE SOFTWARE. 1.9 - org.jenkins-ci - jinterop-wmi - 1.1 - - - org.jenkins-ci - windows-remote-command - 1.4 + org.kohsuke.jinterop + j-interop + 2.0.6-kohsuke-1 org.kohsuke.metainf-services @@ -558,12 +563,6 @@ THE SOFTWARE. provided - - org.kohsuke - owasp-html-sanitizer - r88 - - org.mindrot jbcrypt @@ -583,6 +582,12 @@ THE SOFTWARE. com.google.guava guava + + + com.jcraft + jzlib + 1.1.3-kohsuke-1 + @@ -634,7 +639,6 @@ THE SOFTWARE. com.infradna.tool bridge-method-injector - 1.8 @@ -722,7 +726,7 @@ THE SOFTWARE. com.sun.winsw winsw - 1.13 + 1.16 bin exe ${project.build.outputDirectory}/windows-service @@ -781,7 +785,7 @@ THE SOFTWARE. org.kohsuke.stapler maven-stapler-plugin - 1.16 + ${maven-stapler-plugin.version} /lib/.* diff --git a/core/src/main/groovy/hudson/util/LoadMonitor.groovy b/core/src/main/groovy/hudson/util/LoadMonitor.groovy index 804094d2a4560aacbb2f186f42b42bd3aa309bc1..97d701816199c011e67c6fcc39e539563442f225 100644 --- a/core/src/main/groovy/hudson/util/LoadMonitor.groovy +++ b/core/src/main/groovy/hudson/util/LoadMonitor.groovy @@ -24,6 +24,7 @@ package hudson.util import hudson.model.Computer +import jenkins.util.Timer import jenkins.model.Jenkins import hudson.model.Label import hudson.model.Queue.BlockedItem @@ -31,7 +32,7 @@ import hudson.model.Queue.BuildableItem import hudson.model.Queue.WaitingItem import hudson.triggers.SafeTimerTask import java.text.DateFormat -import hudson.triggers.Trigger; +import java.util.concurrent.TimeUnit /** * Spits out the load information. @@ -51,7 +52,7 @@ public class LoadMonitorImpl extends SafeTimerTask { this.dataFile = dataFile; labels = Jenkins.getInstance().labels*.name; printHeaders(); - Trigger.timer.scheduleAtFixedRate(this,0,10*1000); + Timer.get().scheduleAtFixedRate(this,0,10*1000, TimeUnit.MILLISECONDS); } private String quote(Object s) { "\"${s}\""; } diff --git a/core/src/main/java/hudson/ClassicPluginStrategy.java b/core/src/main/java/hudson/ClassicPluginStrategy.java index e4d158128cf731d42d0bfd220e0fafdeb6e4ed0c..ba123fc95b09f870bdc3a2ba148d48627a3bb008 100644 --- a/core/src/main/java/hudson/ClassicPluginStrategy.java +++ b/core/src/main/java/hudson/ClassicPluginStrategy.java @@ -27,16 +27,15 @@ import com.google.common.collect.Lists; import hudson.Plugin.DummyImpl; import hudson.PluginWrapper.Dependency; import hudson.model.Hudson; +import jenkins.util.AntClassLoader; import hudson.util.CyclicGraphDetector; import hudson.util.CyclicGraphDetector.CycleDetectedException; -import hudson.util.IOException2; import hudson.util.IOUtils; import hudson.util.MaskingClassLoader; import hudson.util.VersionNumber; import jenkins.ClassLoaderReflectionToolkit; import jenkins.ExtensionFilter; import org.apache.commons.io.output.NullOutputStream; -import org.apache.tools.ant.AntClassLoader; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.taskdefs.Expand; @@ -57,7 +56,6 @@ import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -68,12 +66,13 @@ import java.util.HashSet; import java.util.List; import java.util.Vector; import java.util.jar.Attributes; +import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; +import org.jenkinsci.bytecode.Transformer; public class ClassicPluginStrategy implements PluginStrategy { - private final ClassLoaderReflectionToolkit clt = new ClassLoaderReflectionToolkit(); /** * Filter for jar files. @@ -95,15 +94,26 @@ public class ClassicPluginStrategy implements PluginStrategy { this.pluginManager = pluginManager; } - public PluginWrapper createPluginWrapper(File archive) throws IOException { - final Manifest manifest; - URL baseResourceURL; + @Override public String getShortName(File archive) throws IOException { + Manifest manifest; + if (isLinked(archive)) { + manifest = loadLinkedManifest(archive); + } else { + JarFile jf = new JarFile(archive, false); + try { + manifest = jf.getManifest(); + } finally { + jf.close(); + } + } + return PluginWrapper.computeShortName(manifest, archive); + } - File expandDir = null; - // if .hpi, this is the directory where war is expanded + private static boolean isLinked(File archive) { + return archive.getName().endsWith(".hpl") || archive.getName().endsWith(".jpl"); + } - boolean isLinked = archive.getName().endsWith(".hpl") || archive.getName().endsWith(".jpl"); - if (isLinked) { + private static Manifest loadLinkedManifest(File archive) throws IOException { // resolve the .hpl file to the location of the manifest file final String firstLine = IOUtils.readFirstLine(new FileInputStream(archive), "UTF-8"); if (firstLine.startsWith("Manifest-Version:")) { @@ -115,12 +125,24 @@ public class ClassicPluginStrategy implements PluginStrategy { // then parse manifest FileInputStream in = new FileInputStream(archive); try { - manifest = new Manifest(in); + return new Manifest(in); } catch (IOException e) { - throw new IOException2("Failed to load " + archive, e); + throw new IOException("Failed to load " + archive, e); } finally { in.close(); } + } + + @Override public PluginWrapper createPluginWrapper(File archive) throws IOException { + final Manifest manifest; + URL baseResourceURL; + + File expandDir = null; + // if .hpi, this is the directory where war is expanded + + boolean isLinked = isLinked(archive); + if (isLinked) { + manifest = loadLinkedManifest(archive); } else { if (archive.isDirectory()) {// already expanded expandDir = archive; @@ -272,7 +294,11 @@ public class ClassicPluginStrategy implements PluginStrategy { new DetachedPlugin("external-monitor-job","1.467.*","1.0"), new DetachedPlugin("ldap","1.467.*","1.0"), new DetachedPlugin("pam-auth","1.467.*","1.0"), - new DetachedPlugin("mailer","1.493.*","1.2") + new DetachedPlugin("mailer","1.493.*","1.2"), + new DetachedPlugin("matrix-auth","1.535.*","1.0.2"), + new DetachedPlugin("windows-slaves","1.547.*","1.0"), + new DetachedPlugin("antisamy-markup-formatter","1.553.*","1.0"), + new DetachedPlugin("matrix-project","1.561.*","1.0") ); /** @@ -349,13 +375,13 @@ public class ClassicPluginStrategy implements PluginStrategy { } wrapper.setPlugin((Plugin) o); } catch (LinkageError e) { - throw new IOException2("Unable to load " + className + " from " + wrapper.getShortName(),e); + throw new IOException("Unable to load " + className + " from " + wrapper.getShortName(),e); } catch (ClassNotFoundException e) { - throw new IOException2("Unable to load " + className + " from " + wrapper.getShortName(),e); + throw new IOException("Unable to load " + className + " from " + wrapper.getShortName(),e); } catch (IllegalAccessException e) { - throw new IOException2("Unable to create instance of " + className + " from " + wrapper.getShortName(),e); + throw new IOException("Unable to create instance of " + className + " from " + wrapper.getShortName(),e); } catch (InstantiationException e) { - throw new IOException2("Unable to create instance of " + className + " from " + wrapper.getShortName(),e); + throw new IOException("Unable to create instance of " + className + " from " + wrapper.getShortName(),e); } } @@ -366,7 +392,7 @@ public class ClassicPluginStrategy implements PluginStrategy { startPlugin(wrapper); } catch(Throwable t) { // gracefully handle any error in plugin. - throw new IOException2("Failed to initialize",t); + throw new IOException("Failed to initialize",t); } } finally { Thread.currentThread().setContextClassLoader(old); @@ -377,6 +403,36 @@ public class ClassicPluginStrategy implements PluginStrategy { plugin.getPlugin().start(); } + @Override + public void updateDependency(PluginWrapper depender, PluginWrapper dependee) { + DependencyClassLoader classLoader = findAncestorDependencyClassLoader(depender.classLoader); + if (classLoader != null) { + classLoader.updateTransientDependencies(); + LOGGER.log(Level.INFO, "Updated dependency of {0}", depender.getShortName()); + } + } + + private DependencyClassLoader findAncestorDependencyClassLoader(ClassLoader classLoader) + { + for (; classLoader != null; classLoader = classLoader.getParent()) { + if (classLoader instanceof DependencyClassLoader) { + return (DependencyClassLoader)classLoader; + } + + if (classLoader instanceof AntClassLoader) { + // AntClassLoaders hold parents not only as AntClassLoader#getParent() + // but also as AntClassLoader#getConfiguredParent() + DependencyClassLoader ret = findAncestorDependencyClassLoader( + ((AntClassLoader)classLoader).getConfiguredParent() + ); + if (ret != null) { + return ret; + } + } + } + return null; + } + private static File resolve(File base, String relative) { File rel = new File(relative); if(rel.isAbsolute()) @@ -426,7 +482,7 @@ public class ClassicPluginStrategy implements PluginStrategy { unzipExceptClasses(archive, destDir, prj); createClassJarFromWebInfClasses(archive, destDir, prj); } catch (BuildException x) { - throw new IOException2("Failed to expand " + archive,x); + throw new IOException("Failed to expand " + archive,x); } try { @@ -440,7 +496,7 @@ public class ClassicPluginStrategy implements PluginStrategy { * Repackage classes directory into a jar file to make it remoting friendly. * The remoting layer can cache jar files but not class files. */ - private static void createClassJarFromWebInfClasses(File archive, File destDir, Project prj) { + private static void createClassJarFromWebInfClasses(File archive, File destDir, Project prj) throws IOException { File classesJar = new File(destDir, "WEB-INF/lib/classes.jar"); ZipFileSet zfs = new ZipFileSet(); @@ -457,31 +513,36 @@ public class ClassicPluginStrategy implements PluginStrategy { mapper.add(gm); final long dirTime = archive.lastModified(); - Zip z = new Zip() { - /** - * Forces the fixed timestamp for directories to make sure - * classes.jar always get a consistent checksum. - */ - protected void zipDir(Resource dir, ZipOutputStream zOut, String vPath, - int mode, ZipExtraField[] extra) - throws IOException { - - ZipOutputStream wrapped = new ZipOutputStream(new NullOutputStream()) { - @Override - public void putNextEntry(ZipEntry ze) throws IOException { - ze.setTime(dirTime+1999); // roundup - super.putNextEntry(ze); - } - }; - super.zipDir(dir,wrapped,vPath,mode,extra); + // this ZipOutputStream is reused and not created for each directory + final ZipOutputStream wrappedZOut = new ZipOutputStream(new NullOutputStream()) { + @Override + public void putNextEntry(ZipEntry ze) throws IOException { + ze.setTime(dirTime+1999); // roundup + super.putNextEntry(ze); } }; - z.setProject(prj); - z.setTaskType("zip"); - classesJar.getParentFile().mkdirs(); - z.setDestFile(classesJar); - z.add(mapper); - z.execute(); + try { + Zip z = new Zip() { + /** + * Forces the fixed timestamp for directories to make sure + * classes.jar always get a consistent checksum. + */ + protected void zipDir(Resource dir, ZipOutputStream zOut, String vPath, + int mode, ZipExtraField[] extra) + throws IOException { + // use wrappedZOut instead of zOut + super.zipDir(dir,wrappedZOut,vPath,mode,extra); + } + }; + z.setProject(prj); + z.setTaskType("zip"); + classesJar.getParentFile().mkdirs(); + z.setDestFile(classesJar); + z.add(mapper); + z.execute(); + } finally { + wrappedZOut.close(); + } } private static void unzipExceptClasses(File archive, File destDir, Project prj) { @@ -518,6 +579,11 @@ public class ClassicPluginStrategy implements PluginStrategy { this.dependencies = dependencies; } + private void updateTransientDependencies() { + // This will be recalculated at the next time. + transientDependencies = null; + } + private List getTransitiveDependencies() { if (transientDependencies==null) { CyclicGraphDetector cgd = new CyclicGraphDetector() { @@ -562,10 +628,10 @@ public class ClassicPluginStrategy implements PluginStrategy { if (PluginManager.FAST_LOOKUP) { for (PluginWrapper pw : getTransitiveDependencies()) { try { - Class c = clt.findLoadedClass(pw.classLoader,name); + Class c = ClassLoaderReflectionToolkit._findLoadedClass(pw.classLoader, name); if (c!=null) return c; - return clt.findClass(pw.classLoader,name); - } catch (InvocationTargetException e) { + return ClassLoaderReflectionToolkit._findClass(pw.classLoader, name); + } catch (ClassNotFoundException e) { //not found. try next } } @@ -589,15 +655,11 @@ public class ClassicPluginStrategy implements PluginStrategy { HashSet result = new HashSet(); if (PluginManager.FAST_LOOKUP) { - try { for (PluginWrapper pw : getTransitiveDependencies()) { - Enumeration urls = clt.findResources(pw.classLoader, name); + Enumeration urls = ClassLoaderReflectionToolkit._findResources(pw.classLoader, name); while (urls != null && urls.hasMoreElements()) result.add(urls.nextElement()); } - } catch (InvocationTargetException e) { - throw new Error(e); - } } else { for (Dependency dep : dependencies) { PluginWrapper p = pluginManager.getPlugin(dep.shortName); @@ -615,14 +677,10 @@ public class ClassicPluginStrategy implements PluginStrategy { @Override protected URL findResource(String name) { if (PluginManager.FAST_LOOKUP) { - try { for (PluginWrapper pw : getTransitiveDependencies()) { - URL url = clt.findResource(pw.classLoader,name); + URL url = ClassLoaderReflectionToolkit._findResource(pw.classLoader, name); if (url!=null) return url; } - } catch (InvocationTargetException e) { - throw new Error(e); - } } else { for (Dependency dep : dependencies) { PluginWrapper p = pluginManager.getPlugin(dep.shortName); @@ -639,8 +697,7 @@ public class ClassicPluginStrategy implements PluginStrategy { } /** - * {@link AntClassLoader} with a few methods exposed and {@link Closeable} support. - * Deprecated as of Java 7, retained only for Java 5/6. + * {@link AntClassLoader} with a few methods exposed, {@link Closeable} support, and {@link Transformer} support. */ private final class AntClassLoader2 extends AntClassLoader implements Closeable { private final Vector pathComponents; @@ -693,10 +750,13 @@ public class ClassicPluginStrategy implements PluginStrategy { @Override protected Class defineClassFromData(File container, byte[] classData, String classname) throws IOException { - return super.defineClassFromData(container, pluginManager.getCompatibilityTransformer().transform(classname,classData), classname); + if (!DISABLE_TRANSFORMER) + classData = pluginManager.getCompatibilityTransformer().transform(classname, classData); + return super.defineClassFromData(container, classData, classname); } } public static boolean useAntClassLoader = Boolean.getBoolean(ClassicPluginStrategy.class.getName()+".useAntClassLoader"); private static final Logger LOGGER = Logger.getLogger(ClassicPluginStrategy.class.getName()); + public static boolean DISABLE_TRANSFORMER = Boolean.getBoolean(ClassicPluginStrategy.class.getName()+".noBytecodeTransformer"); } diff --git a/core/src/main/java/hudson/DescriptorExtensionList.java b/core/src/main/java/hudson/DescriptorExtensionList.java index 08c814e557db52702a2412b6ec71dd0324d244e0..a3917b2e84b02cd1f9c997f966b9d485977040bc 100644 --- a/core/src/main/java/hudson/DescriptorExtensionList.java +++ b/core/src/main/java/hudson/DescriptorExtensionList.java @@ -35,7 +35,6 @@ import hudson.util.Memoizer; import hudson.util.Iterators.FlattenIterator; import hudson.slaves.NodeDescriptor; import hudson.tasks.Publisher; -import hudson.tasks.Publisher.DescriptorExtensionListImpl; import java.util.Collection; import java.util.List; @@ -44,6 +43,7 @@ import java.util.Iterator; import java.util.logging.Level; import java.util.logging.Logger; import java.util.concurrent.CopyOnWriteArrayList; +import javax.annotation.CheckForNull; import org.kohsuke.stapler.Stapler; import net.sf.json.JSONObject; @@ -72,7 +72,7 @@ public class DescriptorExtensionList, D extends Descrip public static ,D extends Descriptor> DescriptorExtensionList createDescriptorList(Jenkins jenkins, Class describableType) { if (describableType == (Class) Publisher.class) { - return (DescriptorExtensionList) new DescriptorExtensionListImpl(jenkins); + return (DescriptorExtensionList) new Publisher.DescriptorExtensionListImpl(jenkins); } return new DescriptorExtensionList(jenkins,describableType); } @@ -146,7 +146,7 @@ public class DescriptorExtensionList, D extends Descrip * * If none is found, null is returned. */ - public D findByName(String id) { + public @CheckForNull D findByName(String id) { for (D d : this) if(d.getId().equals(id)) return d; diff --git a/core/src/main/java/hudson/EnvVars.java b/core/src/main/java/hudson/EnvVars.java index 0ad75f775d2b339ec5384433dddc4640c5d2b0b3..af8362eeb9a136a6cd877ce44f22feeb1db1c61a 100644 --- a/core/src/main/java/hudson/EnvVars.java +++ b/core/src/main/java/hudson/EnvVars.java @@ -26,13 +26,23 @@ package hudson; import hudson.remoting.Callable; import hudson.remoting.VirtualChannel; import hudson.util.CaseInsensitiveComparator; +import hudson.util.CyclicGraphDetector; +import hudson.util.CyclicGraphDetector.CycleDetectedException; +import hudson.util.VariableResolver; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import java.util.Arrays; +import java.util.TreeSet; import java.util.UUID; +import java.util.logging.Logger; /** * Environment variables. @@ -62,6 +72,7 @@ import java.util.UUID; * @author Kohsuke Kawaguchi */ public class EnvVars extends TreeMap { + private static Logger LOGGER = Logger.getLogger(EnvVars.class.getName()); /** * If this {@link EnvVars} object represents the whole environment variable set, * not just a partial list used for overriding later, then we need to know @@ -148,6 +159,179 @@ public class EnvVars extends TreeMap { return this; } + /** + * Calculates the order to override variables. + * + * Sort variables with topological sort with their reference graph. + * + * This is package accessible for testing purpose. + */ + static class OverrideOrderCalculator { + /** + * Extract variables referred directly from a variable. + */ + private static class TraceResolver implements VariableResolver { + private final Comparator comparator; + public Set referredVariables; + + public TraceResolver(Comparator comparator) { + this.comparator = comparator; + clear(); + } + + public void clear() { + referredVariables = new TreeSet(comparator); + } + + public String resolve(String name) { + referredVariables.add(name); + return ""; + } + } + + private static class VariableReferenceSorter extends CyclicGraphDetector { + // map from a variable to a set of variables that variable refers. + private final Map> refereeSetMap; + + public VariableReferenceSorter(Map> refereeSetMap) { + this.refereeSetMap = refereeSetMap; + } + + @Override + protected Iterable getEdges(String n) { + // return variables referred from the variable. + if (!refereeSetMap.containsKey(n)) { + // there is a case a non-existing variable is referred... + return Collections.emptySet(); + } + return refereeSetMap.get(n); + } + }; + + private final Comparator comparator; + + private final EnvVars target; + private final Map overrides; + + private Map> refereeSetMap; + private List orderedVariableNames; + + public OverrideOrderCalculator(EnvVars target, Map overrides) { + comparator = target.comparator(); + this.target = target; + this.overrides = overrides; + scan(); + } + + public List getOrderedVariableNames() { + return orderedVariableNames; + } + + // Cut the reference to the variable in a cycle. + private void cutCycleAt(String referee, List cycle) { + // cycle contains variables in referrer-to-referee order. + // This should not be negative, for the first and last one is same. + int refererIndex = cycle.lastIndexOf(referee) - 1; + + assert(refererIndex >= 0); + String referrer = cycle.get(refererIndex); + boolean removed = refereeSetMap.get(referrer).remove(referee); + assert(removed); + LOGGER.warning(String.format("Cyclic reference detected: %s", Util.join(cycle," -> "))); + LOGGER.warning(String.format("Cut the reference %s -> %s", referrer, referee)); + } + + // Cut the variable reference in a cycle. + private void cutCycle(List cycle) { + // if an existing variable is contained in that cycle, + // cut the cycle with that variable: + // existing: + // PATH=/usr/bin + // overriding: + // PATH1=/usr/local/bin:${PATH} + // PATH=/opt/something/bin:${PATH1} + // then consider reference PATH1 -> PATH can be ignored. + for (String referee: cycle) { + if (target.containsKey(referee)) { + cutCycleAt(referee, cycle); + return; + } + } + + // if not, cut the reference to the first one. + cutCycleAt(cycle.get(0), cycle); + } + + /** + * Scan all variables and list all referring variables. + */ + public void scan() { + refereeSetMap = new TreeMap>(comparator); + List extendingVariableNames = new ArrayList(); + + TraceResolver resolver = new TraceResolver(comparator); + + for (Map.Entry entry: overrides.entrySet()) { + if (entry.getKey().indexOf('+') > 0) { + // XYZ+AAA variables should be always processed in last. + extendingVariableNames.add(entry.getKey()); + continue; + } + resolver.clear(); + Util.replaceMacro(entry.getValue(), resolver); + + // Variables directly referred from the current scanning variable. + Set refereeSet = resolver.referredVariables; + // Ignore self reference. + refereeSet.remove(entry.getKey()); + refereeSetMap.put(entry.getKey(), refereeSet); + } + + VariableReferenceSorter sorter; + while(true) { + sorter = new VariableReferenceSorter(refereeSetMap); + try { + sorter.run(refereeSetMap.keySet()); + } catch(CycleDetectedException e) { + // cyclic reference found. + // cut the cycle and retry. + @SuppressWarnings("unchecked") + List cycle = e.cycle; + cutCycle(cycle); + continue; + } + break; + } + + // When A refers B, the last appearance of B always comes after + // the last appearance of A. + List reversedDuplicatedOrder = new ArrayList(sorter.getSorted()); + Collections.reverse(reversedDuplicatedOrder); + + orderedVariableNames = new ArrayList(overrides.size()); + for(String key: reversedDuplicatedOrder) { + if(overrides.containsKey(key) && !orderedVariableNames.contains(key)) { + orderedVariableNames.add(key); + } + } + Collections.reverse(orderedVariableNames); + orderedVariableNames.addAll(extendingVariableNames); + } + } + + + /** + * Overrides all values in the map by the given map. Expressions in values will be expanded. + * See {@link #override(String, String)}. + * @return this + */ + public EnvVars overrideExpandingAll(Map all) { + for (String key : new OverrideOrderCalculator(this, all).getOrderedVariableNames()) { + override(key, expand(all.get(key))); + } + return this; + } + /** * Resolves environment variables against each other. */ @@ -172,6 +356,14 @@ public class EnvVars extends TreeMap { if (value==null) throw new IllegalArgumentException("Null value not allowed as an environment variable: "+key); return super.put(key,value); } + + /** + * Add a key/value but only if the value is not-null. Otherwise no-op. + */ + public void putIfNotNull(String key, String value) { + if (value!=null) + put(key,value); + } /** * Takes a string that looks like "a=b" and adds that to this map. diff --git a/core/src/main/java/hudson/ExtensionList.java b/core/src/main/java/hudson/ExtensionList.java index e1a5ca9682e3e0cc9d910fb9acc9464b19f4d8ba..7861de26c2a5b39bc006b922b92476d36cef46c0 100644 --- a/core/src/main/java/hudson/ExtensionList.java +++ b/core/src/main/java/hudson/ExtensionList.java @@ -44,6 +44,8 @@ import java.util.Vector; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; /** * Retains the known extension instances for the given type 'T'. @@ -72,7 +74,7 @@ public class ExtensionList extends AbstractList { * Use {@link #jenkins} */ public final Hudson hudson; - public final Jenkins jenkins; + public final @CheckForNull Jenkins jenkins; public final Class extensionType; /** @@ -119,6 +121,9 @@ public class ExtensionList extends AbstractList { this.jenkins = jenkins; this.extensionType = extensionType; this.legacyInstances = legacyStore; + if (jenkins == null) { + extensions = Collections.emptyList(); + } } /** @@ -176,23 +181,23 @@ public class ExtensionList extends AbstractList { @Override public synchronized boolean remove(Object o) { - removeComponent(legacyInstances,o); + boolean removed = removeComponent(legacyInstances, o); if(extensions!=null) { List> r = new ArrayList>(extensions); - removeComponent(r,o); + removed |= removeComponent(r,o); extensions = sort(r); } - return true; + return removed; } - private void removeComponent(Collection> collection, Object t) { + private boolean removeComponent(Collection> collection, Object t) { for (Iterator> itr = collection.iterator(); itr.hasNext();) { ExtensionComponent c = itr.next(); if (c.getInstance().equals(t)) { - collection.remove(c); - return; + return collection.remove(c); } } + return false; } @Override @@ -240,7 +245,7 @@ public class ExtensionList extends AbstractList { private List> ensureLoaded() { if(extensions!=null) return extensions; // already loaded - if(Jenkins.getInstance().getInitLevel().compareTo(InitMilestone.PLUGINS_PREPARED)<0) + if (jenkins.getInitLevel().compareTo(InitMilestone.PLUGINS_PREPARED)<0) return legacyInstances; // can't perform the auto discovery until all plugins are loaded, so just make the legacy instances visible synchronized (getLoadLock()) { @@ -332,6 +337,20 @@ public class ExtensionList extends AbstractList { } } + /** + * Gets the extension list for a given type. + * Normally calls {@link Jenkins#getExtensionList(Class)} but falls back to an empty list + * in case {@link Jenkins#getInstance} is null. + * Thus it is useful to call from {@code all()} methods which need to behave gracefully during startup or shutdown. + * @param type the extension point type + * @return some list + * @since 1.572 + */ + public static @Nonnull ExtensionList lookup(Class type) { + Jenkins j = Jenkins.getInstance(); + return j == null ? create((Jenkins) null, type) : j.getExtensionList(type); + } + /** * Places to store static-scope legacy instances. */ diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index fe7d95c286940933ca2dda7f1671858d5541efc0..75a9ae1c64c25f7bdfe38f1a0a197c4de8b28809 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -25,42 +25,52 @@ */ package hudson; +import com.jcraft.jzlib.GZIPInputStream; +import com.jcraft.jzlib.GZIPOutputStream; import hudson.Launcher.LocalLauncher; import hudson.Launcher.RemoteLauncher; -import jenkins.model.Jenkins; -import hudson.model.TaskListener; import hudson.model.AbstractProject; +import hudson.model.Computer; import hudson.model.Item; +import hudson.model.TaskListener; +import hudson.org.apache.tools.tar.TarInputStream; +import hudson.os.PosixAPI; +import hudson.os.PosixException; import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.remoting.DelegatingCallable; import hudson.remoting.Future; +import hudson.remoting.LocalChannel; import hudson.remoting.Pipe; +import hudson.remoting.RemoteInputStream; import hudson.remoting.RemoteOutputStream; import hudson.remoting.VirtualChannel; -import hudson.remoting.RemoteInputStream; import hudson.remoting.Which; import hudson.security.AccessControlled; +import hudson.util.DaemonThreadFactory; import hudson.util.DirScanner; -import hudson.util.IOException2; -import hudson.util.HeadBufferingStream; +import hudson.util.ExceptionCatchingThreadFactory; +import hudson.util.FileVisitor; import hudson.util.FormValidation; +import hudson.util.HeadBufferingStream; import hudson.util.IOUtils; - -import static hudson.Util.*; -import static hudson.util.jna.GNUCLibrary.LIBC; -import static hudson.FilePath.TarCompression.GZIP; -import hudson.org.apache.tools.tar.TarInputStream; +import hudson.util.NamingThreadFactory; import hudson.util.io.Archiver; import hudson.util.io.ArchiverFactory; +import jenkins.model.Jenkins; +import jenkins.util.ContextResettingExecutorService; import jenkins.util.VirtualFile; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.io.input.CountingInputStream; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Project; import org.apache.tools.ant.types.FileSet; import org.apache.tools.tar.TarEntry; -import org.apache.commons.io.input.CountingInputStream; -import org.apache.commons.fileupload.FileItem; +import org.apache.tools.zip.ZipEntry; +import org.apache.tools.zip.ZipFile; import org.kohsuke.stapler.Stapler; + +import javax.annotation.CheckForNull; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileFilter; @@ -73,39 +83,34 @@ import java.io.InterruptedIOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.Serializable; import java.io.Writer; -import java.io.OutputStreamWriter; import java.lang.reflect.Field; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; -import java.util.List; -import java.util.StringTokenizer; import java.util.Arrays; import java.util.Comparator; -import java.util.logging.Level; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import com.jcraft.jzlib.GZIPInputStream; -import com.jcraft.jzlib.GZIPOutputStream; - -import com.sun.jna.Native; -import hudson.os.PosixException; -import hudson.util.FileVisitor; -import java.util.Enumeration; -import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; import java.util.logging.Logger; -import org.apache.tools.ant.taskdefs.Chmod; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -import org.apache.tools.zip.ZipFile; -import org.apache.tools.zip.ZipEntry; +import static hudson.FilePath.TarCompression.*; +import static hudson.Util.*; /** * {@link File} like object with remoting support. @@ -199,7 +204,7 @@ public final class FilePath implements Serializable { * that's connected to that machine. If null, that means the local file path. */ public FilePath(VirtualChannel channel, String remote) { - this.channel = channel == Jenkins.MasterComputer.localChannel ? null : channel; + this.channel = channel instanceof LocalChannel ? null : channel; this.remote = normalize(remote); } @@ -392,7 +397,7 @@ public final class FilePath implements Serializable { * * @param glob * Ant style glob, like "**/*.xml". If empty or null, this method - * works like {@link #createZipArchive(OutputStream)} + * works like {@link #createZipArchive(OutputStream)}, inserting a top-level directory into the ZIP. * * @since 1.315 */ @@ -530,7 +535,12 @@ public final class FilePath implements Serializable { if (p != null) { p.mkdirs(); } - IOUtils.copy(zip.getInputStream(e), f); + InputStream input = zip.getInputStream(e); + try { + IOUtils.copy(input, f); + } finally { + input.close(); + } try { FilePath target = new FilePath(f); int mode = e.getUnixMode(); @@ -631,16 +641,11 @@ public final class FilePath implements Serializable { } catch (IOException e) { // various people reported "java.io.IOException: Not in GZIP format" here, so diagnose this problem better in.fillSide(); - throw new IOException2(e.getMessage()+"\nstream="+Util.toHexString(in.getSideBuffer()),e); + throw new IOException(e.getMessage()+"\nstream="+Util.toHexString(in.getSideBuffer()),e); } } public OutputStream compress(OutputStream out) throws IOException { - return new GZIPOutputStream(new BufferedOutputStream(out), - // TODO JENKINS-19473 workaround; replace when jzlib fixed - new com.jcraft.jzlib.Deflater(6, 15+16, 9), // use 9 for memLevel - 512, - true - ); + return new GZIPOutputStream(new BufferedOutputStream(out)); } }; @@ -668,7 +673,7 @@ public final class FilePath implements Serializable { private static final long serialVersionUID = 1L; }); } finally { - IOUtils.closeQuietly(_in); + org.apache.commons.io.IOUtils.closeQuietly(_in); } } @@ -760,13 +765,13 @@ public final class FilePath implements Serializable { else untarFrom(cis,GZIP); } catch (IOException e) { - throw new IOException2(String.format("Failed to unpack %s (%d bytes read of total %d)", + throw new IOException(String.format("Failed to unpack %s (%d bytes read of total %d)", archive,cis.getByteCount(),con.getContentLength()),e); } timestamp.touch(sourceTimestamp); return true; } catch (IOException e) { - throw new IOException2("Failed to install "+archive+" to "+remote,e); + throw new IOException("Failed to install "+archive+" to "+remote,e); } } @@ -786,7 +791,7 @@ public final class FilePath implements Serializable { readFromTar("input stream", dir, GZIP.extract(cis)); } } catch (IOException x) { - throw new IOException2(String.format("Failed to unpack %s (%d bytes read)", archive, cis.getByteCount()), x); + throw new IOException(String.format("Failed to unpack %s (%d bytes read)", archive, cis.getByteCount()), x); } } finally { in.close(); @@ -818,7 +823,7 @@ public final class FilePath implements Serializable { public void copyFrom(InputStream in) throws IOException, InterruptedException { OutputStream os = write(); try { - IOUtils.copy(in, os); + org.apache.commons.io.IOUtils.copy(in, os); } finally { os.close(); } @@ -843,13 +848,13 @@ public final class FilePath implements Serializable { } catch (IOException e) { throw e; } catch (Exception e) { - throw new IOException2(e); + throw new IOException(e); } } else { InputStream i = file.getInputStream(); OutputStream o = write(); try { - IOUtils.copy(i,o); + org.apache.commons.io.IOUtils.copy(i,o); } finally { try { o.close(); @@ -910,11 +915,11 @@ public final class FilePath implements Serializable { throw e; // pass through so that the caller can catch it as AbortException } catch (IOException e) { // wrap it into a new IOException so that we get the caller's stack trace as well. - throw new IOException2("remote file operation failed: "+remote+" at "+channel,e); + throw new IOException("remote file operation failed: "+remote+" at "+channel,e); } } else { // the file is on the local machine. - return callable.invoke(new File(remote), Jenkins.MasterComputer.localChannel); + return callable.invoke(new File(remote), localChannel); } } @@ -988,11 +993,11 @@ public final class FilePath implements Serializable { } } - return (channel!=null ? channel : Jenkins.MasterComputer.localChannel) + return (channel!=null ? channel : localChannel) .callAsync(wrapper); } catch (IOException e) { // wrap it into a new IOException so that we get the caller's stack trace as well. - throw new IOException2("remote file operation failed",e); + throw new IOException("remote file operation failed",e); } } @@ -1053,6 +1058,22 @@ public final class FilePath implements Serializable { return VirtualFile.forFilePath(this); } + /** + * If this {@link FilePath} represents a file on a particular {@link Computer}, return it. + * Otherwise null. + */ + public @CheckForNull Computer toComputer() { + Jenkins j = Jenkins.getInstance(); + if (j != null) { + for (Computer c : j.getComputers()) { + if (getChannel()==c.getChannel()) { + return c; + } + } + } + return null; + } + /** * Creates this directory. */ @@ -1191,7 +1212,7 @@ public final class FilePath implements Serializable { } })); } catch (IOException e) { - throw new IOException2("Failed to create a temp file on "+remote,e); + throw new IOException("Failed to create a temp file on "+remote,e); } } @@ -1251,7 +1272,7 @@ public final class FilePath implements Serializable { try { f = File.createTempFile(prefix, suffix, dir); } catch (IOException e) { - throw new IOException2("Failed to create a temporary directory in "+dir,e); + throw new IOException("Failed to create a temporary directory in "+dir,e); } Writer w = new FileWriter(f); @@ -1265,7 +1286,7 @@ public final class FilePath implements Serializable { } })); } catch (IOException e) { - throw new IOException2("Failed to create a temp file on "+remote,e); + throw new IOException("Failed to create a temp file on "+remote,e); } } @@ -1295,7 +1316,7 @@ public final class FilePath implements Serializable { } })); } catch (IOException e) { - throw new IOException2("Failed to create a temp directory on "+remote,e); + throw new IOException("Failed to create a temp directory on "+remote,e); } } @@ -1409,6 +1430,45 @@ public final class FilePath implements Serializable { }); } + /** + * Returns the number of unallocated bytes in the partition of that file. + * @since 1.542 + */ + public long getFreeDiskSpace() throws IOException, InterruptedException { + return act(new FileCallable() { + private static final long serialVersionUID = 1L; + @Override public Long invoke(File f, VirtualChannel channel) throws IOException { + return f.getFreeSpace(); + } + }); + } + + /** + * Returns the total number of bytes in the partition of that file. + * @since 1.542 + */ + public long getTotalDiskSpace() throws IOException, InterruptedException { + return act(new FileCallable() { + private static final long serialVersionUID = 1L; + @Override public Long invoke(File f, VirtualChannel channel) throws IOException { + return f.getTotalSpace(); + } + }); + } + + /** + * Returns the number of usable bytes in the partition of that file. + * @since 1.542 + */ + public long getUsableDiskSpace() throws IOException, InterruptedException { + return act(new FileCallable() { + private static final long serialVersionUID = 1L; + @Override public Long invoke(File f, VirtualChannel channel) throws IOException { + return f.getUsableSpace(); + } + }); + } + /** * Sets the file permission. * @@ -1437,32 +1497,12 @@ public final class FilePath implements Serializable { } /** - * Run chmod via libc if we can, otherwise fall back to Ant. + * Run chmod via jnr-posix */ private static void _chmod(File f, int mask) throws IOException { if (Functions.isWindows()) return; // noop - try { - if(LIBC.chmod(f.getAbsolutePath(),mask)!=0) { - throw new IOException("Failed to chmod "+f+" : "+LIBC.strerror(Native.getLastError())); - } - } catch(NoClassDefFoundError e) { // cf. https://groups.google.com/group/hudson-dev/browse_thread/thread/6d16c3e8ea0dbc9?hl=fr - _chmodAnt(f, mask); - } catch(UnsatisfiedLinkError e2) { // HUDSON-8155: use Ant's chmod task on non-GNU C systems - _chmodAnt(f, mask); - } - } - - private static void _chmodAnt(File f, int mask) { - if (!CHMOD_WARNED) { // only warn this once to avoid flooding the log - CHMOD_WARNED = true; - LOGGER.warning("GNU C Library not available: Using Ant's chmod task instead."); - } - Chmod chmodTask = new Chmod(); - chmodTask.setProject(new Project()); - chmodTask.setFile(f); - chmodTask.setPerm(Integer.toOctalString(mask)); - chmodTask.execute(); + PosixAPI.jnr().chmod(f.getAbsolutePath(),mask); } private static boolean CHMOD_WARNED = false; @@ -1624,8 +1664,8 @@ public final class FilePath implements Serializable { Util.copyStream(fis,p.getOut()); return null; } finally { - IOUtils.closeQuietly(fis); - IOUtils.closeQuietly(p.getOut()); + org.apache.commons.io.IOUtils.closeQuietly(fis); + org.apache.commons.io.IOUtils.closeQuietly(p.getOut()); } } }); @@ -1639,7 +1679,7 @@ public final class FilePath implements Serializable { public String readToString() throws IOException { InputStream in = read(); try { - return IOUtils.toString(in); + return org.apache.commons.io.IOUtils.toString(in); } finally { in.close(); } @@ -1744,14 +1784,20 @@ public final class FilePath implements Serializable { act(new FileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { + // JENKINS-16846: if f.getName() is the same as one of the files/directories in f, + // then the rename op will fail + File tmp = new File(f.getAbsolutePath()+".__rename"); + if (!f.renameTo(tmp)) + throw new IOException("Failed to rename "+f+" to "+tmp); + File t = new File(target.getRemote()); - for(File child : f.listFiles()) { + for(File child : tmp.listFiles()) { File target = new File(t, child.getName()); if(!child.renameTo(target)) throw new IOException("Failed to rename "+child+" to "+target); } - f.delete(); + tmp.delete(); return null; } }); @@ -1769,7 +1815,7 @@ public final class FilePath implements Serializable { out.close(); } } catch (IOException e) { - throw new IOException2("Failed to copy "+this+" to "+target,e); + throw new IOException("Failed to copy "+this+" to "+target,e); } } @@ -1799,8 +1845,8 @@ public final class FilePath implements Serializable { Util.copyStream(fis,out); return null; } finally { - IOUtils.closeQuietly(fis); - IOUtils.closeQuietly(out); + org.apache.commons.io.IOUtils.closeQuietly(fis); + org.apache.commons.io.IOUtils.closeQuietly(out); } } }); @@ -1917,7 +1963,7 @@ public final class FilePath implements Serializable { @Override public void visit(File f, String relativePath) throws IOException { if (f.isFile()) { File target = new File(dest, relativePath); - target.getParentFile().mkdirs(); + IOUtils.mkdirs(target.getParentFile()); Util.copyFile(f, target); count.incrementAndGet(); } @@ -1927,6 +1973,7 @@ public final class FilePath implements Serializable { } @Override public void visitSymlink(File link, String target, String relativePath) throws IOException { try { + IOUtils.mkdirs(new File(dest, relativePath).getParentFile()); Util.createSymlink(dest, target, relativePath, TaskListener.NULL); } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); @@ -1964,7 +2011,7 @@ public final class FilePath implements Serializable { future.get(); return future2.get(); } catch (ExecutionException e) { - throw new IOException2(e); + throw new IOException(e); } } else { // remote -> local copy @@ -1988,7 +2035,7 @@ public final class FilePath implements Serializable { throw e; // the remote side completed successfully, so the error must be local } catch (ExecutionException x) { // report both errors - throw new IOException2(Functions.printThrowable(e),x); + throw new IOException(Functions.printThrowable(e),x); } catch (TimeoutException _) { // remote is hanging throw e; @@ -1997,7 +2044,7 @@ public final class FilePath implements Serializable { try { return future.get(); } catch (ExecutionException e) { - throw new IOException2(e); + throw new IOException(e); } } } @@ -2069,12 +2116,12 @@ public final class FilePath implements Serializable { } } } catch(IOException e) { - throw new IOException2("Failed to extract "+name,e); + throw new IOException("Failed to extract "+name,e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // process this later - throw new IOException2("Failed to extract "+name,e); + throw new IOException("Failed to extract "+name,e); } catch (IllegalAccessException e) { - throw new IOException2("Failed to extract "+name,e); + throw new IOException("Failed to extract "+name,e); } finally { t.close(); } @@ -2385,7 +2432,7 @@ public final class FilePath implements Serializable { public VirtualChannel getChannel() { if(channel!=null) return channel; - else return Jenkins.MasterComputer.localChannel; + else return localChannel; } /** @@ -2458,7 +2505,7 @@ public final class FilePath implements Serializable { /** * Used to tunnel {@link InterruptedException} over a Java signature that only allows {@link IOException} */ - private static class TunneledInterruptedException extends IOException2 { + private static class TunneledInterruptedException extends IOException { private TunneledInterruptedException(InterruptedException cause) { super(cause); } @@ -2527,4 +2574,11 @@ public final class FilePath implements Serializable { } + private static final ExecutorService threadPoolForRemoting = new ContextResettingExecutorService( + Executors.newCachedThreadPool( + new ExceptionCatchingThreadFactory( + new NamingThreadFactory(new DaemonThreadFactory(), "FilePath.localPool")) + )); + + public static final LocalChannel localChannel = new LocalChannel(threadPoolForRemoting); } diff --git a/core/src/main/java/hudson/FileSystemProvisioner.java b/core/src/main/java/hudson/FileSystemProvisioner.java index 0ab7e44a1eb791749d64ffaa9fe57d98790e0c76..58680eb88847035b1d00790d2c453e5edb82537e 100644 --- a/core/src/main/java/hudson/FileSystemProvisioner.java +++ b/core/src/main/java/hudson/FileSystemProvisioner.java @@ -24,14 +24,12 @@ package hudson; import hudson.FilePath.TarCompression; -import hudson.matrix.MatrixBuild; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Computer; import hudson.model.Describable; import hudson.model.Job; import hudson.model.TaskListener; -import hudson.util.DirScanner.Glob; import hudson.util.io.ArchiverFactory; import jenkins.model.Jenkins; import hudson.model.listeners.RunListener; @@ -158,7 +156,7 @@ public abstract class FileSystemProvisioner implements ExtensionPoint, Describab *

* The state of the build when this method is invoked depends on * the project type. Most would call this at the end of the build, - * but for example {@link MatrixBuild} would call this after + * but for example {@code MatrixBuild} would call this after * SCM check out so that the state of the fresh workspace * can be then propagated to elsewhere. * diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 190143f55de5f9a6ff1f1be575b00f4a31af7bda..b375fad3f3c64c54a0fdb17fe67980cf46e63527 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -28,7 +28,6 @@ package hudson; import hudson.cli.CLICommand; import hudson.console.ConsoleAnnotationDescriptor; import hudson.console.ConsoleAnnotatorFactory; -import hudson.matrix.MatrixProject; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.Describable; @@ -44,9 +43,9 @@ import hudson.model.JobPropertyDescriptor; import hudson.model.ModelObject; import hudson.model.Node; import hudson.model.PageDecorator; +import hudson.model.PaneStatusProperties; import hudson.model.ParameterDefinition; import hudson.model.ParameterDefinition.ParameterDescriptor; -import hudson.model.Project; import hudson.model.Run; import hudson.model.TopLevelItem; import hudson.model.User; @@ -140,7 +139,6 @@ import org.apache.commons.jelly.XMLOutput; import org.apache.commons.jexl.parser.ASTSizeFunction; import org.apache.commons.jexl.util.Introspector; import org.apache.commons.lang.StringUtils; -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.jvnet.tiger_types.Types; import org.kohsuke.stapler.Ancestor; import org.kohsuke.stapler.Stapler; @@ -150,8 +148,10 @@ import org.kohsuke.stapler.jelly.InternationalizedStringExpression.RawHtmlArgume import com.google.common.base.Predicate; import com.google.common.base.Predicates; +import hudson.util.RunList; +import java.util.concurrent.atomic.AtomicLong; import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.DoNotUse; +import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Utility functions used in views. @@ -164,21 +164,16 @@ import org.kohsuke.accmod.restrictions.DoNotUse; */ @SuppressWarnings("rawtypes") public class Functions { - private static volatile int globalIota = 0; - private int iota; + private static final AtomicLong iota = new AtomicLong(); public Functions() { - iota = globalIota; - // concurrent requests can use the same ID --- we are just trying to - // prevent the same user from seeing the same ID repeatedly. - globalIota+=1000; } /** * Generates an unique ID. */ public String generateId() { - return "id"+iota++; + return "id" + iota.getAndIncrement(); } public static boolean isModel(Object o) { @@ -193,11 +188,9 @@ public class Functions { return o instanceof ModelObjectWithChildren; } - /** - * @since 1.524 - */ + @Deprecated public static boolean isMatrixProject(Object o) { - return o instanceof MatrixProject; + return o != null && o.getClass().getName().equals("hudson.matrix.MatrixProject"); } public static String xsDate(Calendar cal) { @@ -209,7 +202,8 @@ public class Functions { } public static void initPageVariables(JellyContext context) { - String rootURL = Stapler.getCurrentRequest().getContextPath(); + StaplerRequest currentRequest = Stapler.getCurrentRequest(); + String rootURL = currentRequest.getContextPath(); Functions h = new Functions(); context.setVariable("h", h); @@ -229,6 +223,8 @@ public class Functions { */ context.setVariable("resURL",rootURL+getResourcePath()); context.setVariable("imagesURL",rootURL+getResourcePath()+"/images"); + + context.setVariable("userAgent", currentRequest.getHeader("User-Agent")); } /** @@ -261,7 +257,7 @@ public class Functions { * like "-5", "+/-0", "+3". */ public static String getDiffString(int i) { - if(i==0) return "\u00B10"; // +/-0 + if(i==0) return "±0"; String s = Integer.toString(i); if(i>0) return "+"+s; else return s; @@ -412,7 +408,7 @@ public class Functions { return Node.Mode.values(); } - public static String getProjectListString(List projects) { + public static String getProjectListString(List projects) { return Items.toNameList(projects); } @@ -449,13 +445,14 @@ public class Functions { return formatter.format(r); } - @Restricted(DoNotUse.class) + @Restricted(NoExternalUse.class) public static String[] printLogRecordHtml(LogRecord r, LogRecord prior) { String[] oldParts = prior == null ? new String[4] : logRecordPreformat(prior); String[] newParts = logRecordPreformat(r); for (int i = 0; i < /* not 4 */3; i++) { newParts[i] = ""; } + newParts[3] = Util.xmlEscape(newParts[3]); return newParts; } /** @@ -512,6 +509,15 @@ public class Functions { return c.getValue(); } + private static final Pattern ICON_SIZE = Pattern.compile("\\d+x\\d+"); + @Restricted(NoExternalUse.class) + public static String validateIconSize(String iconSize) throws SecurityException { + if (!ICON_SIZE.matcher(iconSize).matches()) { + throw new SecurityException("invalid iconSize"); + } + return iconSize; + } + /** * Gets the suffix to use for YUI JavaScript. */ @@ -584,6 +590,10 @@ public class Functions { return false; } + public static boolean isCollapsed(String paneId) { + return PaneStatusProperties.forCurrentUser().isCollapsed(paneId); + } + /** * Finds the given object in the ancestor list and returns its URL. * This is used to determine the "current" URL assigned to the given object, @@ -1003,20 +1013,15 @@ public class Functions { Item i=p; String url = ""; - Collection viewItems; - if (view != null) { - viewItems = view.getItems(); - } else { - viewItems = Collections.emptyList(); - } while(true) { ItemGroup ig = i.getParent(); url = i.getShortUrl()+url; if(ig== Jenkins.getInstance() || (view != null && ig == view.getOwnerItemGroup())) { assert i instanceof TopLevelItem; - if(viewItems.contains((TopLevelItem)i)) { - // if p and the current page belongs to the same view, then return a relative path + if (view != null) { + // assume p and the current page belong to the same view, so return a relative path + // (even if they did not, View.getItem does not by default verify ownership) return normalizeURI(ancestors.get(view)+'/'+url); } else { // otherwise return a path from the root Hudson @@ -1103,7 +1108,7 @@ public class Functions { * @param p the Item we want the relative display name * @param g the ItemGroup used as point of reference for the item * @return - * String like "foo » bar" + * String like "foo/bar" */ public static String getRelativeNameFrom(Item p, ItemGroup g) { return getRelativeNameFrom(p, g, false); @@ -1117,7 +1122,7 @@ public class Functions { * @param p the Item we want the relative display name * @param g the ItemGroup used as point of reference for the item * @return - * String like "foo » bar" + * String like "Foo » Bar" */ public static String getRelativeDisplayNameFrom(Item p, ItemGroup g) { return getRelativeNameFrom(p, g, true); @@ -1129,7 +1134,6 @@ public class Functions { return sorted; } - @IgnoreJRERequirement public static ThreadInfo[] getThreadInfos() { ThreadMXBean mbean = ManagementFactory.getThreadMXBean(); return mbean.dumpAllThreads(mbean.isObjectMonitorUsageSupported(),mbean.isSynchronizerUsageSupported()); @@ -1150,8 +1154,10 @@ public class Functions { while (tg.getParent() != null) tg = tg.getParent(); Thread[] threads = new Thread[tg.activeCount()*2]; int threadsLen = tg.enumerate(threads, true); - for (int i = 0; i < threadsLen; i++) - map.put(threads[i].getId(), threads[i].getThreadGroup().getName()); + for (int i = 0; i < threadsLen; i++) { + ThreadGroup group = threads[i].getThreadGroup(); + map.put(threads[i].getId(), group != null ? group.getName() : null); + } } protected int compare(long idA, long idB) { @@ -1191,20 +1197,14 @@ public class Functions { } /** - * Are we running on JRE6 or above? + * @deprecated Now always true. */ - @IgnoreJRERequirement + @Deprecated public static boolean isMustangOrAbove() { - try { - System.console(); - return true; - } catch(LinkageError e) { - return false; - } + return true; } // ThreadInfo.toString() truncates the stack trace by first 8, so needed my own version - @IgnoreJRERequirement public static String dumpThreadInfo(ThreadInfo ti, ThreadGroupMap map) { String grp = map.getThreadGroup(ti); StringBuilder sb = new StringBuilder("\"" + ti.getThreadName() + "\"" + @@ -1435,7 +1435,8 @@ public class Functions { } /** - * Returns a sub-list if the given list is bigger than the specified 'maxSize' + * Returns a sub-list if the given list is bigger than the specified {@code maxSize}. + * Warning: do not call this with a {@link RunList}, or you will break lazy loading! */ public static List subList(List base, int maxSize) { if(maxSize$1") - .replaceAll("(\\w{10})(?=\\w{3})", "$1") + if (plain == null) { + return null; + } + return plain.replaceAll("([\\p{Punct}&&[^;]]+\\w)", "$1") + .replaceAll("([^\\p{Punct}\\s-]{20})(?=[^\\p{Punct}\\s-]{10})", "$1") ; } @@ -1839,17 +1845,28 @@ public class Functions { */ public static void advertiseHeaders(HttpServletResponse rsp) { Jenkins j = Jenkins.getInstance(); + if (j!=null) { + rsp.setHeader("X-Hudson","1.395"); + rsp.setHeader("X-Jenkins", Jenkins.VERSION); + rsp.setHeader("X-Jenkins-Session", Jenkins.SESSION_HASH); + + TcpSlaveAgentListener tal = j.tcpSlaveAgentListener; + if (tal !=null) { + rsp.setIntHeader("X-Hudson-CLI-Port", tal.getPort()); + rsp.setIntHeader("X-Jenkins-CLI-Port", tal.getPort()); + rsp.setIntHeader("X-Jenkins-CLI2-Port", tal.getPort()); + rsp.setHeader("X-Jenkins-CLI-Host", TcpSlaveAgentListener.CLI_HOST_NAME); + } + } + } - rsp.setHeader("X-Hudson","1.395"); - rsp.setHeader("X-Jenkins", Jenkins.VERSION); - rsp.setHeader("X-Jenkins-Session", Jenkins.SESSION_HASH); - - TcpSlaveAgentListener tal = j.tcpSlaveAgentListener; - if (tal !=null) { - rsp.setIntHeader("X-Hudson-CLI-Port", tal.getPort()); - rsp.setIntHeader("X-Jenkins-CLI-Port", tal.getPort()); - rsp.setIntHeader("X-Jenkins-CLI2-Port", tal.getPort()); - rsp.setHeader("X-Jenkins-CLI-Host", TcpSlaveAgentListener.CLI_HOST_NAME); + @Restricted(NoExternalUse.class) // for actions.jelly and ContextMenu.add + public static boolean isContextMenuVisible(Action a) { + if (a instanceof ModelObjectWithContextMenu.ContextMenuVisibility) { + return ((ModelObjectWithContextMenu.ContextMenuVisibility) a).isVisible(); + } else { + return true; } } + } diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java index 382e850bbd43d26f8ed0f4e762a0b3e24e4ee568..b08dfaf0f139163abb056326c640c50cfbd839b5 100644 --- a/core/src/main/java/hudson/Launcher.java +++ b/core/src/main/java/hudson/Launcher.java @@ -39,6 +39,8 @@ import hudson.util.StreamCopyThread; import hudson.util.ArgumentListBuilder; import hudson.util.ProcessTree; import org.apache.commons.io.input.NullInputStream; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import java.io.BufferedOutputStream; import java.io.File; @@ -298,8 +300,13 @@ public abstract class Launcher { return this; } + /** + * Gets a list of environment variables to be set. + * Returns an empty array if envs field has not been initialized. + * @return If initialized, returns a copy of internal envs array. Otherwise - a new empty array. + */ public String[] envs() { - return envs.clone(); + return envs != null ? envs.clone() : new String[0]; } /** @@ -752,7 +759,7 @@ public abstract class Launcher { */ public static class LocalLauncher extends Launcher { public LocalLauncher(TaskListener listener) { - this(listener, Jenkins.MasterComputer.localChannel); + this(listener, FilePath.localChannel); } public LocalLauncher(TaskListener listener, VirtualChannel channel) { @@ -816,7 +823,7 @@ public abstract class Launcher { * Kill the process when the channel is severed. */ @Override - protected synchronized void terminate(IOException e) { + public synchronized void terminate(IOException e) { super.terminate(e); ProcessTree pt = ProcessTree.get(); try { @@ -841,6 +848,30 @@ public abstract class Launcher { } } + @Restricted(NoExternalUse.class) + public static class DummyLauncher extends Launcher { + + public DummyLauncher(TaskListener listener) { + super(listener, null); + } + + @Override + public Proc launch(ProcStarter starter) throws IOException { + throw new IOException("Can not call launch on a dummy launcher."); + } + + @Override + public Channel launchChannel(String[] cmd, OutputStream out, FilePath workDir, Map envVars) throws IOException, InterruptedException { + throw new IOException("Can not call launchChannel on a dummy launcher."); + } + + @Override + public void kill(Map modelEnvVars) throws IOException, InterruptedException { + // Kill method should do nothing. + } + } + + /** * Launches processes remotely by using the given channel. */ @@ -946,6 +977,88 @@ public abstract class Launcher { } } } + + /** + * A launcher which delegates to a provided inner launcher. + * Allows subclasses to only implement methods they want to override. + * Originally, this launcher has been implemented in + * + * Custom Tools Plugin. + * + * @author rcampbell + * @author Oleg Nenashev, Synopsys Inc. + * @since TODO: define version + */ + public static class DecoratedLauncher extends Launcher { + + private Launcher inner = null; + + public DecoratedLauncher(Launcher inner) { + super(inner); + this.inner = inner; + } + + @Override + public Proc launch(ProcStarter starter) throws IOException { + return inner.launch(starter); + } + + @Override + public Channel launchChannel(String[] cmd, OutputStream out, + FilePath workDir, Map envVars) throws IOException, + InterruptedException { + return inner.launchChannel(cmd, out, workDir, envVars); + } + + @Override + public void kill(Map modelEnvVars) throws IOException, + InterruptedException { + inner.kill(modelEnvVars); + } + + @Override + public boolean isUnix() { + return inner.isUnix(); + } + + @Override + public Proc launch(String[] cmd, boolean[] mask, String[] env, InputStream in, OutputStream out, FilePath workDir) throws IOException { + return inner.launch(cmd, mask, env, in, out, workDir); + } + + @Override + public Computer getComputer() { + return inner.getComputer(); + } + + @Override + public TaskListener getListener() { + return inner.getListener(); + } + + @Override + public String toString() { + return super.toString() + "; decorates " + inner.toString(); + } + + @Override + public VirtualChannel getChannel() { + return inner.getChannel(); + } + + @Override + public Proc launch(String[] cmd, String[] env, InputStream in, OutputStream out, FilePath workDir) throws IOException { + return inner.launch(cmd, env, in, out, workDir); + } + + /** + * Gets nested launcher. + * @return Inner launcher + */ + public Launcher getInner() { + return inner; + } + } public static class IOTriplet implements Serializable { InputStream stdout,stderr; @@ -1087,11 +1200,7 @@ public abstract class Launcher { */ private static EnvVars inherit(Map overrides) { EnvVars m = new EnvVars(EnvVars.masterEnvVars); - // first add all values and then eventually expand them as values can refer other newly added values (see JENKINS-19488) - for (Map.Entry o : overrides.entrySet()) - m.override(o.getKey(),o.getValue()); - for (Map.Entry o : overrides.entrySet()) - m.override(o.getKey(),m.expand(o.getValue())); + m.overrideExpandingAll(overrides); return m; } diff --git a/core/src/main/java/hudson/Main.java b/core/src/main/java/hudson/Main.java index 45067f932baf92499c5e3b1018fe381c19e8326a..cf8a168ba115549ba3e6be45cbdf70423198311d 100644 --- a/core/src/main/java/hudson/Main.java +++ b/core/src/main/java/hudson/Main.java @@ -37,7 +37,6 @@ import java.io.Writer; import java.net.HttpRetryException; import java.net.HttpURLConnection; import java.net.URL; -import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; import java.nio.charset.Charset; @@ -104,14 +103,14 @@ public class Main { } } - String projectNameEnc = URLEncoder.encode(projectName,"UTF-8").replaceAll("\\+","%20"); + URL jobURL = new URL(home + "job/" + Util.encode(projectName).replace("/", "/job/") + "/"); {// check if the job name is correct - HttpURLConnection con = open(new URL(home+"job/"+projectNameEnc+"/acceptBuildResult")); + HttpURLConnection con = open(new URL(jobURL, "acceptBuildResult")); if (auth != null) con.setRequestProperty("Authorization", auth); con.connect(); if(con.getResponseCode()!=200) { - System.err.println(projectName+" is not a valid job name on "+home+" ("+con.getResponseMessage()+")"); + System.err.println(jobURL + " is not a valid external job (" + con.getResponseCode() + " " + con.getResponseMessage() + ")"); return -1; } } @@ -138,29 +137,33 @@ public class Main { FileOutputStream os = new FileOutputStream(tmpFile); Writer w = new OutputStreamWriter(os,"UTF-8"); - w.write(""); - w.write(""); - w.flush(); - - // run the command - long start = System.currentTimeMillis(); - - List cmd = new ArrayList(); - for( int i=1; i"+ret+""+(System.currentTimeMillis()-start)+""); - w.close(); + int ret; + try { + w.write(""); + w.write(""); + w.flush(); + + // run the command + long start = System.currentTimeMillis(); + + List cmd = new ArrayList(); + for( int i=1; i"+ret+""+(System.currentTimeMillis()-start)+""); + } finally { + IOUtils.closeQuietly(w); + } - String location = home+"job/"+projectNameEnc+"/postBuildResult"; + URL location = new URL(jobURL, "postBuildResult"); while(true) { try { // start a remote connection - HttpURLConnection con = open(new URL(location)); + HttpURLConnection con = open(location); if (auth != null) con.setRequestProperty("Authorization", auth); if (crumbField != null && crumbValue != null) { con.setRequestProperty(crumbField, crumbValue); @@ -171,8 +174,11 @@ public class Main { con.connect(); // send the data FileInputStream in = new FileInputStream(tmpFile); - Util.copyStream(in,con.getOutputStream()); - in.close(); + try { + Util.copyStream(in,con.getOutputStream()); + } finally { + IOUtils.closeQuietly(in); + } if(con.getResponseCode()!=200) { Util.copyStream(con.getErrorStream(),System.err); @@ -182,7 +188,7 @@ public class Main { } catch (HttpRetryException e) { if(e.getLocation()!=null) { // retry with the new location - location = e.getLocation(); + location = new URL(e.getLocation()); continue; } // otherwise failed for reasons beyond us. diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 7ce9dea6d3456fbbab2306b7ad951265ae0215c8..4867ba8faeb82082d6e2c57df668d9f88edfc685 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -40,7 +40,6 @@ import hudson.security.Permission; import hudson.security.PermissionScope; import hudson.util.CyclicGraphDetector; import hudson.util.CyclicGraphDetector.CycleDetectedException; -import hudson.util.IOException2; import hudson.util.PersistedList; import hudson.util.Service; import hudson.util.VersionNumber; @@ -83,7 +82,6 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; @@ -110,7 +108,11 @@ import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import static hudson.init.InitMilestone.*; +import hudson.model.DownloadService; +import hudson.util.FormValidation; import static java.util.logging.Level.WARNING; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Manages {@link PluginWrapper}s. @@ -301,7 +303,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas cgd.run(getPlugins()); // obtain topologically sorted list and overwrite the list - ListIterator litr = plugins.listIterator(); + ListIterator litr = getPlugins().listIterator(); for (PluginWrapper p : cgd.getSorted()) { litr.next(); litr.set(p); @@ -410,11 +412,21 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas */ public void dynamicLoad(File arc) throws IOException, InterruptedException, RestartRequiredException { LOGGER.info("Attempting to dynamic load "+arc); - final PluginWrapper p = strategy.createPluginWrapper(arc); - String sn = p.getShortName(); + PluginWrapper p = null; + String sn; + try { + sn = strategy.getShortName(arc); + } catch (AbstractMethodError x) { + LOGGER.log(WARNING, "JENKINS-12753 fix not active: {0}", x.getMessage()); + p = strategy.createPluginWrapper(arc); + sn = p.getShortName(); + } if (getPlugin(sn)!=null) throw new RestartRequiredException(Messages._PluginManager_PluginIsAlreadyInstalled_RestartRequired(sn)); + if (p == null) { + p = strategy.createPluginWrapper(arc); + } if (p.supportsDynamicLoad()== YesNoMaybe.NO) throw new RestartRequiredException(Messages._PluginManager_PluginDoesntSupportDynamicLoad_RestartRequired(sn)); @@ -435,22 +447,44 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas failedPlugins.add(new FailedPlugin(sn, e)); activePlugins.remove(p); plugins.remove(p); - throw new IOException2("Failed to install "+ sn +" plugin",e); + throw new IOException("Failed to install "+ sn +" plugin",e); } // run initializers in the added plugin Reactor r = new Reactor(InitMilestone.ordering()); - r.addAll(new InitializerFinder(p.classLoader) { + final ClassLoader loader = p.classLoader; + r.addAll(new InitializerFinder(loader) { @Override protected boolean filter(Method e) { - return e.getDeclaringClass().getClassLoader()!=p.classLoader || super.filter(e); + return e.getDeclaringClass().getClassLoader() != loader || super.filter(e); } }.discoverTasks(r)); try { new InitReactorRunner().run(r); } catch (ReactorException e) { - throw new IOException2("Failed to initialize "+ sn +" plugin",e); + throw new IOException("Failed to initialize "+ sn +" plugin",e); } + + // recalculate dependencies of plugins optionally depending the newly deployed one. + for (PluginWrapper depender: plugins) { + if (depender.equals(p)) { + // skip itself. + continue; + } + for (Dependency d: depender.getOptionalDependencies()) { + if (d.shortName.equals(p.getShortName())) { + // this plugin depends on the newly loaded one! + // recalculate dependencies! + try { + getPluginStrategy().updateDependency(depender, p); + } catch (AbstractMethodError x) { + LOGGER.log(WARNING, "{0} does not yet implement updateDependency", getPluginStrategy().getClass()); + } + break; + } + } + } + LOGGER.info("Plugin " + sn + " dynamically installed"); } @@ -553,7 +587,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas */ @Exported public List getPlugins() { - return plugins; + List out = new ArrayList(plugins.size()); + out.addAll(plugins); + return out; } public List getFailedPlugins() { @@ -566,7 +602,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas * @return The plugin singleton or null if a plugin with the given short name does not exist. */ public PluginWrapper getPlugin(String shortName) { - for (PluginWrapper p : plugins) { + for (PluginWrapper p : getPlugins()) { if(p.getShortName().equals(shortName)) return p; } @@ -580,7 +616,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas * @return The plugin singleton or null if for some reason the plugin is not loaded. */ public PluginWrapper getPlugin(Class pluginClazz) { - for (PluginWrapper p : plugins) { + for (PluginWrapper p : getPlugins()) { if(pluginClazz.isInstance(p.getPlugin())) return p; } @@ -595,7 +631,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas */ public List getPlugins(Class pluginSuperclass) { List result = new ArrayList(); - for (PluginWrapper p : plugins) { + for (PluginWrapper p : getPlugins()) { if(pluginSuperclass.isInstance(p.getPlugin())) result.add(p); } @@ -686,13 +722,33 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas String n = en.nextElement(); if(n.startsWith("plugin.")) { n = n.substring(7); - if (n.indexOf(".") > 0) { - String[] pluginInfo = n.split("\\."); - UpdateSite.Plugin p = Jenkins.getInstance().getUpdateCenter().getById(pluginInfo[1]).getPlugin(pluginInfo[0]); - if(p==null) - throw new Failure("No such plugin: "+n); - p.deploy(dynamicLoad); + // JENKINS-22080 plugin names can contain '.' as could (according to rumour) update sites + int index = n.indexOf('.'); + UpdateSite.Plugin p = null; + while (index != -1) { + if (index + 1 >= n.length()) { + break; + } + String pluginName = n.substring(0, index); + String siteName = n.substring(index + 1); + UpdateSite updateSite = Jenkins.getInstance().getUpdateCenter().getById(siteName); + if (updateSite == null) { + throw new Failure("No such update center: " + siteName); + } else { + UpdateSite.Plugin plugin = updateSite.getPlugin(pluginName); + if (plugin != null) { + if (p != null) { + throw new Failure("Ambiguous plugin: " + n); + } + p = plugin; + } + } + index = n.indexOf('.', index + 1); } + if (p == null) { + throw new Failure("No such plugin: " + n); + } + p.deploy(dynamicLoad); } } rsp.sendRedirect("../updateCenter/"); @@ -777,6 +833,24 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas } } + @Restricted(NoExternalUse.class) + @RequirePOST public HttpResponse doCheckUpdatesServer() throws IOException { + for (UpdateSite site : Jenkins.getInstance().getUpdateCenter().getSites()) { + FormValidation v = site.updateDirectlyNow(DownloadService.signatureCheck); + if (v.kind != FormValidation.Kind.OK) { + // TODO crude but enough for now + return v; + } + } + for (DownloadService.Downloadable d : DownloadService.Downloadable.all()) { + FormValidation v = d.updateNow(); + if (v.kind != FormValidation.Kind.OK) { + return v; + } + } + return HttpResponses.forwardToPreviousPage(); + } + protected String identifyPluginShortName(File t) { try { JarFile j = new JarFile(t); @@ -925,7 +999,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas } }); } catch (SAXException x) { - throw new IOException2("Failed to parse XML",x); + throw new IOException("Failed to parse XML",x); } catch (ParserConfigurationException e) { throw new AssertionError(e); // impossible since we don't tweak XMLParser } @@ -942,8 +1016,6 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas */ private ConcurrentMap> generatedClasses = new ConcurrentHashMap>(); - private ClassLoaderReflectionToolkit clt = new ClassLoaderReflectionToolkit(); - public UberClassLoader() { super(PluginManager.class.getClassLoader()); } @@ -964,11 +1036,11 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas if (FAST_LOOKUP) { for (PluginWrapper p : activePlugins) { try { - Class c = clt.findLoadedClass(p.classLoader,name); + Class c = ClassLoaderReflectionToolkit._findLoadedClass(p.classLoader, name); if (c!=null) return c; // calling findClass twice appears to cause LinkageError: duplicate class def - return clt.findClass(p.classLoader,name); - } catch (InvocationTargetException e) { + return ClassLoaderReflectionToolkit._findClass(p.classLoader, name); + } catch (ClassNotFoundException e) { //not found. try next } } @@ -988,15 +1060,11 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas @Override protected URL findResource(String name) { if (FAST_LOOKUP) { - try { for (PluginWrapper p : activePlugins) { - URL url = clt.findResource(p.classLoader,name); + URL url = ClassLoaderReflectionToolkit._findResource(p.classLoader, name); if(url!=null) return url; } - } catch (InvocationTargetException e) { - throw new Error(e); - } } else { for (PluginWrapper p : activePlugins) { URL url = p.classLoader.getResource(name); @@ -1011,13 +1079,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas protected Enumeration findResources(String name) throws IOException { List resources = new ArrayList(); if (FAST_LOOKUP) { - try { for (PluginWrapper p : activePlugins) { - resources.addAll(Collections.list(clt.findResources(p.classLoader, name))); + resources.addAll(Collections.list(ClassLoaderReflectionToolkit._findResources(p.classLoader, name))); } - } catch (InvocationTargetException e) { - throw new Error(e); - } } else { for (PluginWrapper p : activePlugins) { resources.addAll(Collections.list(p.classLoader.getResources(name))); diff --git a/core/src/main/java/hudson/PluginStrategy.java b/core/src/main/java/hudson/PluginStrategy.java index 6024c3571f4cb382b5cebeedc2e3e4acce028095..2acb1f706555ac5fed9f4ed2d2c0bd2c4ffef26a 100644 --- a/core/src/main/java/hudson/PluginStrategy.java +++ b/core/src/main/java/hudson/PluginStrategy.java @@ -24,11 +24,10 @@ package hudson; import hudson.model.Hudson; -import jenkins.model.Jenkins; - import java.io.File; import java.io.IOException; import java.util.List; +import javax.annotation.Nonnull; /** * Pluggability point for how to create {@link PluginWrapper}. @@ -50,6 +49,13 @@ public interface PluginStrategy extends ExtensionPoint { PluginWrapper createPluginWrapper(File archive) throws IOException; + /** + * Finds the plugin name without actually unpacking anything {@link #createPluginWrapper} would. + * Needed by {@link PluginManager#dynamicLoad} to decide whether such a plugin is already installed. + * @return the {@link PluginWrapper#getShortName} + */ + @Nonnull String getShortName(File archive) throws IOException; + /** * Loads the plugin and starts it. * @@ -77,4 +83,14 @@ public interface PluginStrategy extends ExtensionPoint { * @since 1.400 */ List> findComponents(Class type, Hudson hudson); + + /** + * Called when a plugin is installed, but there was already a plugin installed which optionally depended on that plugin. + * The class loader of the existing depending plugin should be updated + * to load classes from the newly installed plugin. + * @param depender plugin depending on dependee. + * @param dependee newly loaded plugin. + * @since 1.557 + */ + void updateDependency(PluginWrapper depender, PluginWrapper dependee); } diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java index dc8fd6659db1d6e2678b09a01cce4d36b5c1aa13..b7364499a4b6ac4717b68b7b3870a81864020177 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -45,6 +45,7 @@ import java.util.jar.Manifest; import java.util.logging.Logger; import static java.util.logging.Level.WARNING; +import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.LogFactory; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; @@ -54,6 +55,8 @@ import org.kohsuke.stapler.interceptor.RequirePOST; import java.util.Enumeration; import java.util.jar.JarFile; +import java.util.logging.Level; +import javax.annotation.CheckForNull; /** * Represents a Jenkins plug-in and associated control information @@ -211,7 +214,7 @@ public class PluginWrapper implements Comparable, ModelObject { } public String getDisplayName() { - return getLongName(); + return StringUtils.removeStart(getLongName(), "Jenkins "); } public Api getApi() { @@ -235,7 +238,7 @@ public class PluginWrapper implements Comparable, ModelObject { return idx != null && idx.toString().contains(shortName) ? idx : null; } - private String computeShortName(Manifest manifest, File archive) { + static String computeShortName(Manifest manifest, File archive) { // use the name captured in the manifest, as often plugins // depend on the specific short name in its URLs. String n = manifest.getMainAttributes().getValue("Short-Name"); @@ -283,8 +286,9 @@ public class PluginWrapper implements Comparable, ModelObject { /** * Gets the instance of {@link Plugin} contributed by this plugin. */ - public Plugin getPlugin() { - return Jenkins.lookup(PluginInstanceStore.class).store.get(this); + public @CheckForNull Plugin getPlugin() { + PluginInstanceStore pis = Jenkins.lookup(PluginInstanceStore.class); + return pis != null ? pis.store.get(this) : null; } /** @@ -372,11 +376,16 @@ public class PluginWrapper implements Comparable, ModelObject { * Terminates the plugin. */ public void stop() { - LOGGER.info("Stopping "+shortName); - try { - getPlugin().stop(); - } catch(Throwable t) { - LOGGER.log(WARNING, "Failed to shut down "+shortName, t); + Plugin plugin = getPlugin(); + if (plugin != null) { + try { + LOGGER.log(Level.FINE, "Stopping {0}", shortName); + plugin.stop(); + } catch (Throwable t) { + LOGGER.log(WARNING, "Failed to shut down " + shortName, t); + } + } else { + LOGGER.log(Level.FINE, "Could not find Plugin instance to stop for {0}", shortName); } // Work around a bug in commons-logging. // See http://www.szegedi.org/articles/memleak.html @@ -573,7 +582,7 @@ public class PluginWrapper implements Comparable, ModelObject { backupPlugin.close(); } } catch (IOException e) { - LOGGER.log(WARNING, "Failed to get backup version ", e); + LOGGER.log(WARNING, "Failed to get backup version from " + backup, e); return null; } } else { diff --git a/core/src/main/java/hudson/Proc.java b/core/src/main/java/hudson/Proc.java index ca1f4a6767d26fedc768acb33f866f0bf0ece38b..5114cbd2c445a9e490efe2477f7d396932296ad3 100644 --- a/core/src/main/java/hudson/Proc.java +++ b/core/src/main/java/hudson/Proc.java @@ -28,7 +28,7 @@ import hudson.model.TaskListener; import hudson.remoting.Channel; import hudson.util.DaemonThreadFactory; import hudson.util.ExceptionCatchingThreadFactory; -import hudson.util.IOException2; +import hudson.util.NamingThreadFactory; import hudson.util.NullStream; import hudson.util.StreamCopyThread; import hudson.util.ProcessTree; @@ -132,7 +132,7 @@ public abstract class Proc { */ public abstract OutputStream getStdin(); - private static final ExecutorService executor = Executors.newCachedThreadPool(new ExceptionCatchingThreadFactory(new DaemonThreadFactory())); + private static final ExecutorService executor = Executors.newCachedThreadPool(new ExceptionCatchingThreadFactory(new NamingThreadFactory(new DaemonThreadFactory(), "Proc.executor"))); /** * Like {@link #join} but can be given a maximum time to wait. @@ -454,7 +454,7 @@ public abstract class Proc { } catch (ExecutionException e) { if(e.getCause() instanceof IOException) throw (IOException)e.getCause(); - throw new IOException2("Failed to join the process",e); + throw new IOException("Failed to join the process",e); } catch (CancellationException x) { return -1; } diff --git a/core/src/main/java/hudson/ProxyConfiguration.java b/core/src/main/java/hudson/ProxyConfiguration.java index d4e10f65c4010a9ae214ad0cc74c8e569f560406..0e36a4cd9d946d9bfa1f0e97a55bca3852deedf9 100644 --- a/core/src/main/java/hudson/ProxyConfiguration.java +++ b/core/src/main/java/hudson/ProxyConfiguration.java @@ -24,32 +24,35 @@ package hudson; import com.google.common.collect.Lists; +import com.thoughtworks.xstream.XStream; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; -import hudson.util.FormValidation; -import jenkins.model.Jenkins; import hudson.model.Saveable; import hudson.model.listeners.SaveableListener; +import hudson.util.FormValidation; import hudson.util.Scrambler; import hudson.util.Secret; import hudson.util.XStream2; - import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.Serializable; import java.net.Authenticator; +import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.PasswordAuthentication; import java.net.Proxy; import java.net.URL; import java.net.URLConnection; - -import com.thoughtworks.xstream.XStream; -import java.io.InputStream; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; - +import jenkins.model.Jenkins; +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.UsernamePasswordCredentials; +import org.apache.commons.httpclient.auth.AuthScope; +import org.apache.commons.httpclient.methods.GetMethod; import org.jvnet.robust_http_client.RetryableHttpStream; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; @@ -65,7 +68,7 @@ import org.kohsuke.stapler.QueryParameter; * (as described in the Java 6 tech note * * Http Authentication). - * + * * @see jenkins.model.Jenkins#proxy */ public final class ProxyConfiguration extends AbstractDescribableImpl implements Saveable, Serializable { @@ -80,7 +83,7 @@ public final class ProxyConfiguration extends AbstractDescribableImpl List filter( Iterable base, Class type ) { + @Nonnull + public static List filter( @Nonnull Iterable base, @Nonnull Class type ) { List r = new ArrayList(); for (Object i : base) { if(type.isInstance(i)) @@ -107,7 +110,8 @@ public class Util { /** * Creates a filtered sublist. */ - public static List filter( List base, Class type ) { + @Nonnull + public static List filter( @Nonnull List base, @Nonnull Class type ) { return filter((Iterable)base,type); } @@ -123,7 +127,8 @@ public class Util { * Unlike shell, undefined variables are left as-is (this behavior is the same as Ant.) * */ - public static String replaceMacro(String s, Map properties) { + @Nullable + public static String replaceMacro( @CheckForNull String s, @Nonnull Map properties) { return replaceMacro(s,new VariableResolver.ByMap(properties)); } @@ -133,7 +138,8 @@ public class Util { *

* Unlike shell, undefined variables are left as-is (this behavior is the same as Ant.) */ - public static String replaceMacro(String s, VariableResolver resolver) { + @Nullable + public static String replaceMacro(@CheckForNull String s, @Nonnull VariableResolver resolver) { if (s == null) { return null; } @@ -166,11 +172,13 @@ public class Util { /** * Loads the contents of a file into a string. */ - public static String loadFile(File logfile) throws IOException { + @Nonnull + public static String loadFile(@Nonnull File logfile) throws IOException { return loadFile(logfile, Charset.defaultCharset()); } - public static String loadFile(File logfile,Charset charset) throws IOException { + @Nonnull + public static String loadFile(@Nonnull File logfile, @Nonnull Charset charset) throws IOException { if(!logfile.exists()) return ""; @@ -196,7 +204,7 @@ public class Util { * @throws IOException * if the operation fails. */ - public static void deleteContentsRecursive(File file) throws IOException { + public static void deleteContentsRecursive(@Nonnull File file) throws IOException { File[] files = file.listFiles(); if(files==null) return; // the directory didn't exist in the first place @@ -209,7 +217,7 @@ public class Util { * @param f a file to delete * @throws IOException if it exists but could not be successfully deleted */ - public static void deleteFile(File f) throws IOException { + public static void deleteFile(@Nonnull File f) throws IOException { if (!f.delete()) { if(!f.exists()) // we are trying to delete a file that no longer exists, so this is not an error @@ -260,16 +268,11 @@ public class Util { /** * Makes the given file writable by any means possible. */ - @IgnoreJRERequirement - private static void makeWritable(File f) { - // try JDK6-way of doing it. - try { - if (f.setWritable(true)) { - return; - } - } catch (NoSuchMethodError e) { - // not JDK6 + private static void makeWritable(@Nonnull File f) { + if (f.setWritable(true)) { + return; } + // TODO do we still need to try anything else? // try chmod. this becomes no-op if this is not Unix. try { @@ -293,7 +296,7 @@ public class Util { } - public static void deleteRecursive(File dir) throws IOException { + public static void deleteRecursive(@Nonnull File dir) throws IOException { if(!isSymlink(dir)) deleteContentsRecursive(dir); try { @@ -327,7 +330,7 @@ public class Util { * Checks if the given file represents a symlink. */ //Taken from http://svn.apache.org/viewvc/maven/shared/trunk/file-management/src/main/java/org/apache/maven/shared/model/fileset/util/FileSetManager.java?view=markup - public static boolean isSymlink(File file) throws IOException { + public static boolean isSymlink(@Nonnull File file) throws IOException { Boolean r = isSymlinkJava7(file); if (r != null) { return r; @@ -356,12 +359,12 @@ public class Util { } @SuppressWarnings("NP_BOOLEAN_RETURN_NULL") - private static Boolean isSymlinkJava7(File file) throws IOException { + private static Boolean isSymlinkJava7(@Nonnull File file) throws IOException { try { Object path = File.class.getMethod("toPath").invoke(file); return (Boolean) Class.forName("java.nio.file.Files").getMethod("isSymbolicLink", Class.forName("java.nio.file.Path")).invoke(null, path); } catch (NoSuchMethodException x) { - return null; // fine, Java 5/6 + return null; // fine, Java 6 } catch (Exception x) { throw (IOException) new IOException(x.toString()).initCause(x); } @@ -385,13 +388,14 @@ public class Util { * On Windows, error messages for IOException aren't very helpful. * This method generates additional user-friendly error message to the listener */ - public static void displayIOException( IOException e, TaskListener listener ) { + public static void displayIOException(@Nonnull IOException e, @Nonnull TaskListener listener ) { String msg = getWin32ErrorMessage(e); if(msg!=null) listener.getLogger().println(msg); } - public static String getWin32ErrorMessage(IOException e) { + @CheckForNull + public static String getWin32ErrorMessage(@Nonnull IOException e) { return getWin32ErrorMessage((Throwable)e); } @@ -401,6 +405,7 @@ public class Util { * @return * null if there seems to be no error code or if the platform is not Win32. */ + @CheckForNull public static String getWin32ErrorMessage(Throwable e) { String msg = e.getMessage(); if(msg!=null) { @@ -426,6 +431,7 @@ public class Util { * @return * null if no such message is available. */ + @CheckForNull public static String getWin32ErrorMessage(int n) { try { ResourceBundle rb = ResourceBundle.getBundle("/hudson/win32errors"); @@ -439,6 +445,7 @@ public class Util { /** * Guesses the current host name. */ + @Nonnull public static String getHostName() { try { return InetAddress.getLocalHost().getHostName(); @@ -447,21 +454,21 @@ public class Util { } } - public static void copyStream(InputStream in,OutputStream out) throws IOException { + public static void copyStream(@Nonnull InputStream in,@Nonnull OutputStream out) throws IOException { byte[] buf = new byte[8192]; int len; while((len=in.read(buf))>=0) out.write(buf,0,len); } - public static void copyStream(Reader in, Writer out) throws IOException { + public static void copyStream(@Nonnull Reader in, @Nonnull Writer out) throws IOException { char[] buf = new char[8192]; int len; while((len=in.read(buf))>0) out.write(buf,0,len); } - public static void copyStreamAndClose(InputStream in,OutputStream out) throws IOException { + public static void copyStreamAndClose(@Nonnull InputStream in, @Nonnull OutputStream out) throws IOException { try { copyStream(in,out); } finally { @@ -470,7 +477,7 @@ public class Util { } } - public static void copyStreamAndClose(Reader in,Writer out) throws IOException { + public static void copyStreamAndClose(@Nonnull Reader in, @Nonnull Writer out) throws IOException { try { copyStream(in,out); } finally { @@ -489,18 +496,21 @@ public class Util { * @since 1.145 * @see QuotedStringTokenizer */ - public static String[] tokenize(String s,String delimiter) { + @Nonnull + public static String[] tokenize(@Nonnull String s, @CheckForNull String delimiter) { return QuotedStringTokenizer.tokenize(s,delimiter); } - public static String[] tokenize(String s) { + @Nonnull + public static String[] tokenize(@Nonnull String s) { return tokenize(s," \t\n\r\f"); } /** * Converts the map format of the environment variables to the K=V format in the array. */ - public static String[] mapToEnv(Map m) { + @Nonnull + public static String[] mapToEnv(@Nonnull Map m) { String[] r = new String[m.size()]; int idx=0; @@ -510,7 +520,7 @@ public class Util { return r; } - public static int min(int x, int... values) { + public static int min(int x, @Nonnull int... values) { for (int i : values) { if(i List createSubList( Collection source, Class type ) { + @Nonnull + public static List createSubList(@Nonnull Collection source, @Nonnull Class type ) { List r = new ArrayList(); for (Object item : source) { if(type.isInstance(item)) @@ -755,7 +780,8 @@ public class Util { * {@link #rawEncode(String)} should generally be used instead, though be careful to pass only * a single path component to that method (it will encode /, but this method does not). */ - public static String encode(String s) { + @Nonnull + public static String encode(@Nonnull String s) { try { boolean escaped = false; @@ -812,7 +838,8 @@ public class Util { * single path component used in constructing a URL. * Method name inspired by PHP's rawurlencode. */ - public static String rawEncode(String s) { + @Nonnull + public static String rawEncode(@Nonnull String s) { boolean escaped = false; StringBuilder out = null; CharsetEncoder enc = null; @@ -861,7 +888,8 @@ public class Util { /** * Escapes HTML unsafe characters like <, & to the respective character entities. */ - public static String escape(String text) { + @Nonnull + public static String escape(@Nonnull String text) { if (text==null) return null; StringBuilder buf = new StringBuilder(text.length()+64); for( int i=0; i List fixNull(List l) { + @Nonnull + public static List fixNull(@CheckForNull List l) { return l!=null ? l : Collections.emptyList(); } - public static Set fixNull(Set l) { + @Nonnull + public static Set fixNull(@CheckForNull Set l) { return l!=null ? l : Collections.emptySet(); } - public static Collection fixNull(Collection l) { + @Nonnull + public static Collection fixNull(@CheckForNull Collection l) { return l!=null ? l : Collections.emptySet(); } - public static Iterable fixNull(Iterable l) { + @Nonnull + public static Iterable fixNull(@CheckForNull Iterable l) { return l!=null ? l : Collections.emptySet(); } /** * Cuts all the leading path portion and get just the file name. */ - public static String getFileName(String filePath) { + @Nonnull + public static String getFileName(@Nonnull String filePath) { int idx = filePath.lastIndexOf('\\'); if(idx>=0) return getFileName(filePath.substring(idx+1)); @@ -986,7 +1023,8 @@ public class Util { /** * Concatenate multiple strings by inserting a separator. */ - public static String join(Collection strings, String separator) { + @Nonnull + public static String join(@Nonnull Collection strings, @Nonnull String separator) { StringBuilder buf = new StringBuilder(); boolean first=true; for (Object s : strings) { @@ -1000,7 +1038,8 @@ public class Util { /** * Combines all the given collections into a single list. */ - public static List join(Collection... items) { + @Nonnull + public static List join(@Nonnull Collection... items) { int size = 0; for (Collection item : items) size += item.size(); @@ -1027,7 +1066,8 @@ public class Util { * Can be null. * @since 1.172 */ - public static FileSet createFileSet(File baseDir, String includes, String excludes) { + @Nonnull + public static FileSet createFileSet(@Nonnull File baseDir, @Nonnull String includes, @CheckForNull String excludes) { FileSet fs = new FileSet(); fs.setDir(baseDir); fs.setProject(new Project()); @@ -1049,7 +1089,8 @@ public class Util { return fs; } - public static FileSet createFileSet(File baseDir, String includes) { + @Nonnull + public static FileSet createFileSet(@Nonnull File baseDir, @Nonnull String includes) { return createFileSet(baseDir,includes,null); } @@ -1065,7 +1106,8 @@ public class Util { * @param symlinkPath * Where to create a symlink in (relative to {@code baseDir}) */ - public static void createSymlink(File baseDir, String targetPath, String symlinkPath, TaskListener listener) throws InterruptedException { + public static void createSymlink(@Nonnull File baseDir, @Nonnull String targetPath, + @Nonnull String symlinkPath, @Nonnull TaskListener listener) throws InterruptedException { try { if (createSymlinkJava7(baseDir, targetPath, symlinkPath)) { return; @@ -1137,18 +1179,37 @@ public class Util { } } - private static boolean createSymlinkJava7(File baseDir, String targetPath, String symlinkPath) throws IOException { + private static boolean createSymlinkJava7(@Nonnull File baseDir, @Nonnull String targetPath, @Nonnull String symlinkPath) throws IOException { try { Object path = File.class.getMethod("toPath").invoke(new File(baseDir, symlinkPath)); Object target = Class.forName("java.nio.file.Paths").getMethod("get", String.class, String[].class).invoke(null, targetPath, new String[0]); Class filesC = Class.forName("java.nio.file.Files"); Class pathC = Class.forName("java.nio.file.Path"); - filesC.getMethod("deleteIfExists", pathC).invoke(null, path); + Class fileAlreadyExistsExceptionC = Class.forName("java.nio.file.FileAlreadyExistsException"); + Object noAttrs = Array.newInstance(Class.forName("java.nio.file.attribute.FileAttribute"), 0); - filesC.getMethod("createSymbolicLink", pathC, pathC, noAttrs.getClass()).invoke(null, path, target, noAttrs); + final int maxNumberOfTries = 4; + final int timeInMillis = 100; + for (int tryNumber = 1; tryNumber <= maxNumberOfTries; tryNumber++) { + filesC.getMethod("deleteIfExists", pathC).invoke(null, path); + try { + filesC.getMethod("createSymbolicLink", pathC, pathC, noAttrs.getClass()).invoke(null, path, target, noAttrs); + break; + } + catch (Exception x) { + if (fileAlreadyExistsExceptionC.isInstance(x)) { + if(tryNumber < maxNumberOfTries) { + TimeUnit.MILLISECONDS.sleep(timeInMillis); //trying to defeat likely ongoing race condition + continue; + } + LOGGER.warning("symlink FileAlreadyExistsException thrown "+maxNumberOfTries+" times => cannot createSymbolicLink"); + } + throw x; + } + } return true; } catch (NoSuchMethodException x) { - return false; // fine, Java 5/6 + return false; // fine, Java 6 } catch (InvocationTargetException x) { Throwable x2 = x.getCause(); if (x2 instanceof UnsupportedOperationException) { @@ -1188,7 +1249,8 @@ public class Util { * @return null * if the specified file is not a symlink. */ - public static File resolveSymlinkToFile(File link) throws InterruptedException, IOException { + @CheckForNull + public static File resolveSymlinkToFile(@Nonnull File link) throws InterruptedException, IOException { String target = resolveSymlink(link); if (target==null) return null; @@ -1208,12 +1270,13 @@ public class Util { * If the symlink is relative, the returned string is that relative representation. * The relative path is meant to be resolved from the location of the symlink. */ - public static String resolveSymlink(File link) throws InterruptedException, IOException { + @CheckForNull + public static String resolveSymlink(@Nonnull File link) throws InterruptedException, IOException { try { // Java 7 Object path = File.class.getMethod("toPath").invoke(link); return Class.forName("java.nio.file.Files").getMethod("readSymbolicLink", Class.forName("java.nio.file.Path")).invoke(null, path).toString(); } catch (NoSuchMethodException x) { - // fine, Java 5/6; fall through + // fine, Java 6; fall through } catch (InvocationTargetException x) { Throwable x2 = x.getCause(); if (x2 instanceof UnsupportedOperationException) { @@ -1293,7 +1356,8 @@ public class Util { * Wraps with the error icon and the CSS class to render error message. * @since 1.173 */ - public static String wrapToErrorSpan(String s) { + @Nonnull + public static String wrapToErrorSpan(@Nonnull String s) { s = ""+s+""; return s; } @@ -1306,7 +1370,8 @@ public class Util { * @param defaultNumber number to return if the string can not be parsed * @return returns the parsed string; otherwise the default number */ - public static Number tryParseNumber(String numberStr, Number defaultNumber) { + @CheckForNull + public static Number tryParseNumber(@CheckForNull String numberStr, @CheckForNull Number defaultNumber) { if ((numberStr == null) || (numberStr.length() == 0)) { return defaultNumber; } @@ -1321,7 +1386,7 @@ public class Util { * Checks if the public method defined on the base type with the given arguments * are overridden in the given derived type. */ - public static boolean isOverridden(Class base, Class derived, String methodName, Class... types) { + public static boolean isOverridden(@Nonnull Class base, @Nonnull Class derived, @Nonnull String methodName, @Nonnull Class... types) { try { return !base.getMethod(methodName, types).equals( derived.getMethod(methodName,types)); @@ -1336,7 +1401,8 @@ public class Util { * @param ext * For example, ".zip" */ - public static File changeExtension(File dst, String ext) { + @Nonnull + public static File changeExtension(@Nonnull File dst, @Nonnull String ext) { String p = dst.getPath(); int pos = p.lastIndexOf('.'); if (pos<0) return new File(p+ext); @@ -1345,8 +1411,10 @@ public class Util { /** * Null-safe String intern method. + * @return A canonical representation for the string object. Null for null input strings */ - public static String intern(String s) { + @Nullable + public static String intern(@CheckForNull String s) { return s==null ? s : s.intern(); } @@ -1357,7 +1425,7 @@ public class Util { * implementing this by ourselves allow it to be more lenient about * escaping of URI. */ - public static boolean isAbsoluteUri(String uri) { + public static boolean isAbsoluteUri(@Nonnull String uri) { int idx = uri.indexOf(':'); if (idx<0) return false; // no ':'. can't be absolute @@ -1369,7 +1437,7 @@ public class Util { * Works like {@link String#indexOf(int)} but 'not found' is returned as s.length(), not -1. * This enables more straight-forward comparison. */ - private static int _indexOf(String s, char ch) { + private static int _indexOf(@Nonnull String s, char ch) { int idx = s.indexOf(ch); if (idx<0) return s.length(); return idx; @@ -1379,17 +1447,10 @@ public class Util { * Loads a key/value pair string as {@link Properties} * @since 1.392 */ - @IgnoreJRERequirement - public static Properties loadProperties(String properties) throws IOException { + @Nonnull + public static Properties loadProperties(@Nonnull String properties) throws IOException { Properties p = new Properties(); - try { - p.load(new StringReader(properties)); - } catch (NoSuchMethodError e) { - // load(Reader) method is only available on JDK6. - // this fall back version doesn't work correctly with non-ASCII characters, - // but there's no other easy ways out it seems. - p.load(new ByteArrayInputStream(properties.getBytes())); - } + p.load(new StringReader(properties)); return p; } diff --git a/core/src/main/java/hudson/WebAppMain.java b/core/src/main/java/hudson/WebAppMain.java index f6cf979bf80de2edf8e446706d6e480bb064de0f..1f332e97513444f678216d599a2ddee8a3903965 100644 --- a/core/src/main/java/hudson/WebAppMain.java +++ b/core/src/main/java/hudson/WebAppMain.java @@ -25,7 +25,9 @@ package hudson; import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider; import com.thoughtworks.xstream.core.JVM; +import com.trilead.ssh2.util.IOUtils; import hudson.model.Hudson; +import hudson.util.BootFailure; import jenkins.model.Jenkins; import hudson.util.HudsonIsLoading; import hudson.util.IncompatibleServletVersionDetected; @@ -52,21 +54,25 @@ import javax.servlet.ServletResponse; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; +import java.util.Date; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import java.security.Security; import java.util.logging.LogRecord; +import static java.util.logging.Level.*; + /** * Entry point when Hudson is used as a webapp. * * @author Kohsuke Kawaguchi */ -public final class WebAppMain implements ServletContextListener { +public class WebAppMain implements ServletContextListener { private final RingBufferLogHandler handler = new RingBufferLogHandler() { @Override public synchronized void publish(LogRecord record) { if (record.getLevel().intValue() >= Level.INFO.intValue()) { @@ -82,8 +88,9 @@ public final class WebAppMain implements ServletContextListener { * Creates the sole instance of {@link jenkins.model.Jenkins} and register it to the {@link ServletContext}. */ public void contextInitialized(ServletContextEvent event) { + final ServletContext context = event.getServletContext(); + File home=null; try { - final ServletContext context = event.getServletContext(); // use the current request to determine the language LocaleProvider.setProvider(new LocaleProvider() { @@ -98,8 +105,7 @@ public final class WebAppMain implements ServletContextListener { jvm = new JVM(); new URLClassLoader(new URL[0],getClass().getClassLoader()); } catch(SecurityException e) { - context.setAttribute(APP,new InsufficientPermissionDetected(e)); - return; + throw new InsufficientPermissionDetected(e); } try {// remove Sun PKCS11 provider if present. See http://wiki.jenkins-ci.org/display/JENKINS/Solaris+Issue+6276483 @@ -111,21 +117,19 @@ public final class WebAppMain implements ServletContextListener { installLogger(); final FileAndDescription describedHomeDir = getHomeDir(event); - final File home = describedHomeDir.file.getAbsoluteFile(); + home = describedHomeDir.file.getAbsoluteFile(); home.mkdirs(); System.out.println("Jenkins home directory: "+home+" found at: "+describedHomeDir.description); // check that home exists (as mkdirs could have failed silently), otherwise throw a meaningful error - if (! home.exists()) { - context.setAttribute(APP,new NoHomeDir(home)); - return; - } + if (!home.exists()) + throw new NoHomeDir(home); + + recordBootAttempt(home); // make sure that we are using XStream in the "enhanced" (JVM-specific) mode if(jvm.bestReflectionProvider().getClass()==PureJavaReflectionProvider.class) { - // nope - context.setAttribute(APP,new IncompatibleVMDetected()); - return; + throw new IncompatibleVMDetected(); // nope } // JNA is no longer a hard requirement. It's just nice to have. See HUDSON-4820 for more context. @@ -163,22 +167,19 @@ public final class WebAppMain implements ServletContextListener { try { ServletResponse.class.getMethod("setCharacterEncoding",String.class); } catch (NoSuchMethodException e) { - context.setAttribute(APP,new IncompatibleServletVersionDetected(ServletResponse.class)); - return; + throw new IncompatibleServletVersionDetected(ServletResponse.class); } // make sure that we see Ant 1.7 try { FileSet.class.getMethod("getDirectoryScanner"); } catch (NoSuchMethodException e) { - context.setAttribute(APP,new IncompatibleAntVersionDetected(FileSet.class)); - return; + throw new IncompatibleAntVersionDetected(FileSet.class); } // make sure AWT is functioning, or else JFreeChart won't even load. if(ChartUtil.awtProblemCause!=null) { - context.setAttribute(APP,new AWTProblem(ChartUtil.awtProblemCause)); - return; + throw new AWTProblem(ChartUtil.awtProblemCause); } // some containers (in particular Tomcat) doesn't abort a launch @@ -188,8 +189,7 @@ public final class WebAppMain implements ServletContextListener { File f = File.createTempFile("test", "test"); f.delete(); } catch (IOException e) { - context.setAttribute(APP,new NoTempDir(e)); - return; + throw new NoTempDir(e); } // Tomcat breaks XSLT with JDK 5.0 and onward. Check if that's the case, and if so, @@ -199,13 +199,13 @@ public final class WebAppMain implements ServletContextListener { // if this works we are all happy } catch (TransformerFactoryConfigurationError x) { // no it didn't. - LOGGER.log(Level.WARNING, "XSLT not configured correctly. Hudson will try to fix this. See http://issues.apache.org/bugzilla/show_bug.cgi?id=40895 for more details",x); + LOGGER.log(WARNING, "XSLT not configured correctly. Hudson will try to fix this. See http://issues.apache.org/bugzilla/show_bug.cgi?id=40895 for more details",x); System.setProperty(TransformerFactory.class.getName(),"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"); try { TransformerFactory.newInstance(); LOGGER.info("XSLT is set to the JAXP RI in JRE"); } catch(TransformerFactoryConfigurationError y) { - LOGGER.log(Level.SEVERE, "Failed to correct the problem."); + LOGGER.log(SEVERE, "Failed to correct the problem."); } } @@ -213,24 +213,25 @@ public final class WebAppMain implements ServletContextListener { context.setAttribute(APP,new HudsonIsLoading()); + final File _home = home; initThread = new Thread("Jenkins initialization thread") { @Override public void run() { boolean success = false; try { - Jenkins instance = new Hudson(home, context); + Jenkins instance = new Hudson(_home, context); context.setAttribute(APP, instance); + BootFailure.getBootFailureFile(_home).delete(); + // at this point we are open for business and serving requests normally LOGGER.info("Jenkins is fully up and running"); success = true; } catch (Error e) { - LOGGER.log(Level.SEVERE, "Failed to initialize Jenkins",e); - context.setAttribute(APP,new HudsonFailedToLoad(e)); + new HudsonFailedToLoad(e).publish(context,_home); throw e; } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Failed to initialize Jenkins",e); - context.setAttribute(APP,new HudsonFailedToLoad(e)); + new HudsonFailedToLoad(e).publish(context,_home); } finally { Jenkins instance = Jenkins.getInstance(); if(!success && instance!=null) @@ -239,15 +240,39 @@ public final class WebAppMain implements ServletContextListener { } }; initThread.start(); + } catch (BootFailure e) { + e.publish(context,home); } catch (Error e) { - LOGGER.log(Level.SEVERE, "Failed to initialize Jenkins",e); + LOGGER.log(SEVERE, "Failed to initialize Jenkins",e); throw e; } catch (RuntimeException e) { - LOGGER.log(Level.SEVERE, "Failed to initialize Jenkins",e); + LOGGER.log(SEVERE, "Failed to initialize Jenkins",e); throw e; } } + public void joinInit() throws InterruptedException { + initThread.join(); + } + + /** + * To assist boot failure script, record the number of boot attempts. + * This file gets deleted in case of successful boot. + * + * @see BootFailure + */ + private void recordBootAttempt(File home) { + FileOutputStream o=null; + try { + o = new FileOutputStream(BootFailure.getBootFailureFile(home), true); + o.write((new Date().toString() + System.getProperty("line.separator", "\n")).toString().getBytes()); + } catch (IOException e) { + LOGGER.log(WARNING, "Failed to record boot attempts",e); + } finally { + IOUtils.closeQuietly(o); + } + } + public static void installExpressionFactory(ServletContextEvent event) { JellyFacet.setExpressionFactory(event, new ExpressionFactory2()); } diff --git a/core/src/main/java/hudson/XmlFile.java b/core/src/main/java/hudson/XmlFile.java index 18a95412dbaca0aa3be66147a13eb805aca495b6..00eae060e9f378a4631ab505bc3e9daedb39fad2 100644 --- a/core/src/main/java/hudson/XmlFile.java +++ b/core/src/main/java/hudson/XmlFile.java @@ -24,7 +24,7 @@ package hudson; import com.thoughtworks.xstream.XStream; -import com.thoughtworks.xstream.converters.ConversionException; +import com.thoughtworks.xstream.XStreamException; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.StreamException; @@ -32,7 +32,6 @@ import com.thoughtworks.xstream.io.xml.XppDriver; import hudson.diagnosis.OldDataMonitor; import hudson.model.Descriptor; import hudson.util.AtomicFileWriter; -import hudson.util.IOException2; import hudson.util.XStream2; import org.xml.sax.Attributes; import org.xml.sax.InputSource; @@ -141,12 +140,10 @@ public final class XmlFile { InputStream in = new BufferedInputStream(new FileInputStream(file)); try { return xs.fromXML(in); - } catch(StreamException e) { - throw new IOException2("Unable to read "+file,e); - } catch(ConversionException e) { - throw new IOException2("Unable to read "+file,e); + } catch (XStreamException e) { + throw new IOException("Unable to read "+file,e); } catch(Error e) {// mostly reflection errors - throw new IOException2("Unable to read "+file,e); + throw new IOException("Unable to read "+file,e); } finally { in.close(); } @@ -164,12 +161,10 @@ public final class XmlFile { try { // TODO: expose XStream the driver from XStream return xs.unmarshal(DEFAULT_DRIVER.createReader(in), o); - } catch (StreamException e) { - throw new IOException2("Unable to read "+file,e); - } catch(ConversionException e) { - throw new IOException2("Unable to read "+file,e); + } catch (XStreamException e) { + throw new IOException("Unable to read "+file,e); } catch(Error e) {// mostly reflection errors - throw new IOException2("Unable to read "+file,e); + throw new IOException("Unable to read "+file,e); } finally { in.close(); } @@ -183,7 +178,7 @@ public final class XmlFile { xs.toXML(o,w); w.commit(); } catch(StreamException e) { - throw new IOException2(e); + throw new IOException(e); } finally { w.abort(); } @@ -294,7 +289,7 @@ public final class XmlFile { // in such a case, assume UTF-8 rather than fail, since Jenkins internally always write XML in UTF-8 return "UTF-8"; } catch (SAXException e) { - throw new IOException2("Failed to detect encoding of "+file,e); + throw new IOException("Failed to detect encoding of "+file,e); } catch (ParserConfigurationException e) { throw new AssertionError(e); // impossible } finally { diff --git a/core/src/main/java/hudson/cli/AddJobToViewCommand.java b/core/src/main/java/hudson/cli/AddJobToViewCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..cea483785a304ea5651758c4f078e8b8d2cb18b3 --- /dev/null +++ b/core/src/main/java/hudson/cli/AddJobToViewCommand.java @@ -0,0 +1,68 @@ +/* + * The MIT License + * + * Copyright (c) 2014 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import java.util.List; + +import hudson.Extension; +import hudson.model.TopLevelItem; +import hudson.model.DirectlyModifiableView; +import hudson.model.View; + +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.CmdLineException; + +/** + * @author ogondza + * @since 1.570 + */ +@Extension +public class AddJobToViewCommand extends CLICommand { + + @Argument(usage="Name of the view", required=true, index=0) + private View view; + + @Argument(usage="Job names", required=true, index=1) + private List jobs; + + @Override + public String getShortDescription() { + return Messages.AddJobToViewCommand_ShortDescription(); + } + + @Override + protected int run() throws Exception { + view.checkPermission(View.CONFIGURE); + + if (!(view instanceof DirectlyModifiableView)) throw new CmdLineException( + null, "'" + view.getDisplayName() + "' view can not be modified directly" + ); + + for (TopLevelItem job: jobs) { + ((DirectlyModifiableView) view).add(job); + } + + return 0; + } +} diff --git a/core/src/main/java/hudson/cli/BuildCommand.java b/core/src/main/java/hudson/cli/BuildCommand.java index 8ee62ac1345d65ff76a2ccbeab4c61f2768b3c96..06093e110d987ba558d2cdc3cef4336ff979f03a 100644 --- a/core/src/main/java/hudson/cli/BuildCommand.java +++ b/core/src/main/java/hudson/cli/BuildCommand.java @@ -23,6 +23,7 @@ */ package hudson.cli; +import hudson.Util; import hudson.console.ModelHyperlinkNote; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; @@ -50,6 +51,7 @@ import java.util.ArrayList; import java.util.Map.Entry; import java.io.FileNotFoundException; import java.io.PrintStream; +import javax.annotation.Nonnull; import jenkins.model.Jenkins; @@ -86,11 +88,10 @@ public class BuildCommand extends CLICommand { @Option(name="-v",usage="Prints out the console output of the build. Use with -s") public boolean consoleOutput = false; - @Option(name="-r", usage="Number of times to retry reading of the output log if it does not exists on first attempt. Defaults to 0. Use with -v.") - public String retryCntStr = "0"; + @Option(name="-r") @Deprecated + public int retryCnt = 10; - // hold parsed retryCnt; - private int retryCnt = 0; + protected static final String BUILD_SCHEDULING_REFUSED = "Build scheduling Refused by an extension, hence not in Queue"; protected int run() throws Exception { job.checkPermission(Item.BUILD); @@ -101,15 +102,21 @@ public class BuildCommand extends CLICommand { if (pdp==null) throw new AbortException(job.getFullDisplayName()+" is not parameterized but the -p option was specified"); + //TODO: switch to type annotations after the migration to Java 1.8 List values = new ArrayList(); for (Entry e : parameters.entrySet()) { String name = e.getKey(); ParameterDefinition pd = pdp.getParameterDefinition(name); - if (pd==null) + if (pd==null) { throw new AbortException(String.format("\'%s\' is not a valid parameter. Did you mean %s?", name, EditDistance.findNearest(name, pdp.getParameterDefinitionNames()))); - values.add(pd.createValue(this,e.getValue())); + } + ParameterValue val = pd.createValue(this, Util.fixNull(e.getValue())); + if (val == null) { + throw new AbortException(String.format("Cannot resolve the value for the parameter \'%s\'.",name)); + } + values.add(val); } // handle missing parameters by adding as default values ISSUE JENKINS-7162 @@ -118,14 +125,16 @@ public class BuildCommand extends CLICommand { continue; // not passed in use default - values.add(pd.getDefaultParameterValue()); + ParameterValue defaultValue = pd.getDefaultParameterValue(); + if (defaultValue == null) { + throw new AbortException(String.format("No default value for the parameter \'%s\'.",pd.getName())); + } + values.add(defaultValue); } a = new ParametersAction(values); } - retryCnt = Integer.parseInt(retryCntStr); - if (checkSCM) { if (job.poll(new StreamTaskListener(stdout, getClientCharset())).change == Change.NONE) { return 0; @@ -146,6 +155,10 @@ public class BuildCommand extends CLICommand { QueueTaskFuture f = job.scheduleBuild2(0, new CLICause(Jenkins.getAuthentication().getName()), a); if (wait || sync || follow) { + if (f == null) { + stderr.println(BUILD_SCHEDULING_REFUSED); + return -1; + } AbstractBuild b = f.waitForStart(); // wait for the start stdout.println("Started "+b.getFullDisplayName()); diff --git a/core/src/main/java/hudson/cli/CLIAction.java b/core/src/main/java/hudson/cli/CLIAction.java new file mode 100644 index 0000000000000000000000000000000000000000..3253fc2840074f4674e69d3f45456716f0692fcd --- /dev/null +++ b/core/src/main/java/hudson/cli/CLIAction.java @@ -0,0 +1,136 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +import hudson.model.UnprotectedRootAction; +import jenkins.model.Jenkins; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.HttpResponses.HttpResponseException; +import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.StaplerProxy; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import hudson.Extension; +import hudson.model.FullDuplexHttpChannel; +import hudson.remoting.Channel; + +/** + * Shows usage of CLI and commands. + * + * @author ogondza + */ +@Extension +@Restricted(NoExternalUse.class) +public class CLIAction implements UnprotectedRootAction, StaplerProxy { + + private transient final Map duplexChannels = new HashMap(); + + public String getIconFileName() { + return null; + } + + public String getDisplayName() { + + return "Jenkins CLI"; + } + + public String getUrlName() { + return "cli"; + } + + public void doCommand(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { + final Jenkins jenkins = Jenkins.getInstance(); + jenkins.checkPermission(Jenkins.READ); + + // Strip trailing slash + final String commandName = req.getRestOfPath().substring(1); + CLICommand command = CLICommand.clone(commandName); + if (command == null) { + rsp.sendError(HttpServletResponse.SC_NOT_FOUND, "No such command " + commandName); + return; + } + + req.setAttribute("command", command); + req.getView(this, "command.jelly").forward(req, rsp); + } + + @Override + public Object getTarget() { + StaplerRequest req = Stapler.getCurrentRequest(); + if (req.getRestOfPath().length()==0 && "POST".equals(req.getMethod())) { + // CLI connection request + throw new CliEndpointResponse(); + } else { + return this; + } + } + + /** + * Serves CLI-over-HTTP response. + */ + private class CliEndpointResponse extends HttpResponseException { + @Override + public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { + try { + // do not require any permission to establish a CLI connection + // the actual authentication for the connecting Channel is done by CLICommand + + UUID uuid = UUID.fromString(req.getHeader("Session")); + rsp.setHeader("Hudson-Duplex",""); // set the header so that the client would know + + FullDuplexHttpChannel server; + if(req.getHeader("Side").equals("download")) { + duplexChannels.put(uuid,server=new FullDuplexHttpChannel(uuid, !Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) { + @Override + protected void main(Channel channel) throws IOException, InterruptedException { + // capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator() + channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION, Jenkins.getAuthentication()); + channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel)); + } + }); + try { + server.download(req,rsp); + } finally { + duplexChannels.remove(uuid); + } + } else { + duplexChannels.get(uuid).upload(req,rsp); + } + } catch (InterruptedException e) { + throw new IOException(e); + } + } + } +} diff --git a/core/src/main/java/hudson/cli/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java index c5a672ff40bb73074d65b7451f873b9e82c31e16..dc79a622de0a5e2437abf9c325cbb9da877f4935 100644 --- a/core/src/main/java/hudson/cli/CLICommand.java +++ b/core/src/main/java/hudson/cli/CLICommand.java @@ -46,12 +46,15 @@ import org.apache.commons.discovery.resource.classes.DiscoverClasses; import org.apache.commons.discovery.resource.names.DiscoverServiceNames; import org.jvnet.hudson.annotation_indexer.Index; import org.jvnet.tiger_types.Types; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.args4j.ClassParser; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.spi.OptionHandler; import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; @@ -186,6 +189,8 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { * The default implementation uses args4j to parse command line arguments and call {@link #run()}, * but if that processing is undesirable, subtypes can directly override this method and leave {@link #run()} * to an empty method. + * You would however then have to consider {@link CliAuthenticator} and {@link #getTransportAuthentication}, + * so this is not really recommended. * * @param args * Arguments to the sub command. For example, if the CLI is invoked like "java -jar cli.jar foo bar zot", @@ -208,7 +213,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { this.stderr = stderr; this.locale = locale; registerOptionHandlers(); - CmdLineParser p = new CmdLineParser(this); + CmdLineParser p = getCmdLineParser(); // add options from the authenticator SecurityContext sc = SecurityContextHolder.getContext(); @@ -242,6 +247,16 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { sc.setAuthentication(old); // restore } } + + /** + * Get parser for this command. + * + * Exposed to be overridden by {@link hudson.cli.declarative.CLIRegisterer}. + * @since 1.538 + */ + protected CmdLineParser getCmdLineParser() { + return new CmdLineParser(this); + } public Channel checkChannel() throws AbortException { if (channel==null) @@ -329,11 +344,46 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { protected abstract int run() throws Exception; protected void printUsage(PrintStream stderr, CmdLineParser p) { - stderr.println("java -jar jenkins-cli.jar "+getName()+" args..."); + stderr.print("java -jar jenkins-cli.jar " + getName()); + p.printSingleLineUsage(stderr); + stderr.println(); printUsageSummary(stderr); p.printUsage(stderr); } + /** + * Get single line summary as a string. + */ + @Restricted(NoExternalUse.class) + public final String getSingleLineSummary() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + getCmdLineParser().printSingleLineUsage(out); + return out.toString(); + } + + /** + * Get usage as a string. + */ + @Restricted(NoExternalUse.class) + public final String getUsage() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + getCmdLineParser().printUsage(out); + return out.toString(); + } + + /** + * Get long description as a string. + */ + @Restricted(NoExternalUse.class) + public final String getLongDescription() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(out); + + printUsageSummary(ps); + ps.close(); + return out.toString(); + } + /** * Called while producing usage. This is a good method to override * to render the general description of the command that goes beyond diff --git a/core/src/main/java/hudson/cli/CliProtocol.java b/core/src/main/java/hudson/cli/CliProtocol.java index b3b725fe33ff896a3c59b1a167c682c935c7fe00..1d4f25627e077f237fa41dc731605309e7c72b59 100644 --- a/core/src/main/java/hudson/cli/CliProtocol.java +++ b/core/src/main/java/hudson/cli/CliProtocol.java @@ -4,9 +4,13 @@ import hudson.Extension; import hudson.model.Computer; import hudson.remoting.Channel; import hudson.remoting.Channel.Mode; +import hudson.remoting.ChannelBuilder; import jenkins.AgentProtocol; import jenkins.model.Jenkins; +import jenkins.slaves.NioChannelSelector; +import org.jenkinsci.remoting.nio.NioChannelHub; +import javax.inject.Inject; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedWriter; @@ -23,6 +27,9 @@ import java.net.Socket; */ @Extension public class CliProtocol extends AgentProtocol { + @Inject + NioChannelSelector nio; + @Override public String getName() { return "CLI-connect"; @@ -30,13 +37,23 @@ public class CliProtocol extends AgentProtocol { @Override public void handle(Socket socket) throws IOException, InterruptedException { - new Handler(socket).run(); + new Handler(nio.getHub(),socket).run(); } protected static class Handler { + protected final NioChannelHub hub; protected final Socket socket; + /** + * @deprecated as of 1.559 + * Use {@link #Handler(NioChannelHub, Socket)} + */ public Handler(Socket socket) { + this(null,socket); + } + + public Handler(NioChannelHub hub, Socket socket) { + this.hub = hub; this.socket = socket; } @@ -47,9 +64,21 @@ public class CliProtocol extends AgentProtocol { } protected void runCli(Connection c) throws IOException, InterruptedException { - Channel channel = new Channel("CLI channel from " + socket.getInetAddress(), - Computer.threadPoolForRemoting, Mode.BINARY, - new BufferedInputStream(c.in), new BufferedOutputStream(c.out), null, true, Jenkins.getInstance().pluginManager.uberClassLoader); + ChannelBuilder cb; + String name = "CLI channel from " + socket.getInetAddress(); + + // Connection can contain cipher wrapper, which can't be NIO-ed. +// if (hub!=null) +// cb = hub.newChannelBuilder(name, Computer.threadPoolForRemoting); +// else + cb = new ChannelBuilder(name, Computer.threadPoolForRemoting); + + Channel channel = cb + .withMode(Mode.BINARY) + .withRestricted(true) + .withBaseLoader(Jenkins.getInstance().pluginManager.uberClassLoader) + .build(new BufferedInputStream(c.in), new BufferedOutputStream(c.out)); + channel.setProperty(CliEntryPoint.class.getName(),new CliManagerImpl(channel)); channel.join(); } diff --git a/core/src/main/java/hudson/cli/CliProtocol2.java b/core/src/main/java/hudson/cli/CliProtocol2.java index f1cea1add398c40724aa2ad3df51ec425bd752e1..644914a089bdaa2bb2368f8d2c9bf940b5987cc0 100644 --- a/core/src/main/java/hudson/cli/CliProtocol2.java +++ b/core/src/main/java/hudson/cli/CliProtocol2.java @@ -1,8 +1,8 @@ package hudson.cli; import hudson.Extension; -import hudson.util.IOException2; import jenkins.model.Jenkins; +import org.jenkinsci.remoting.nio.NioChannelHub; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -29,14 +29,22 @@ public class CliProtocol2 extends CliProtocol { @Override public void handle(Socket socket) throws IOException, InterruptedException { - new Handler2(socket).run(); + new Handler2(nio.getHub(), socket).run(); } protected static class Handler2 extends Handler { + /** + * @deprecated as of 1.559 + * Use {@link #Handler2(NioChannelHub, Socket)} + */ public Handler2(Socket socket) { super(socket); } + public Handler2(NioChannelHub hub, Socket socket) { + super(hub, socket); + } + @Override public void run() throws IOException, InterruptedException { try { @@ -72,7 +80,7 @@ public class CliProtocol2 extends CliProtocol { runCli(c); } catch (GeneralSecurityException e) { - throw new IOException2("Failed to encrypt the CLI channel",e); + throw new IOException("Failed to encrypt the CLI channel",e); } } } diff --git a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java index cd727e7ba42eb906aec2adf02d16ce0fe0587412..3f9479c6e75827b052f55129970a3b8ca91a202b 100644 --- a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java +++ b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java @@ -1,11 +1,10 @@ package hudson.cli; import hudson.FilePath; -import jenkins.model.Jenkins; -import jenkins.model.Jenkins.MasterComputer; import hudson.remoting.Callable; import hudson.remoting.Channel; import hudson.util.Secret; +import jenkins.model.Jenkins; import org.acegisecurity.Authentication; import org.acegisecurity.AuthenticationException; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; @@ -40,10 +39,14 @@ public class ClientAuthenticationCache implements Serializable { private final Properties props = new Properties(); public ClientAuthenticationCache(Channel channel) throws IOException, InterruptedException { - store = (channel==null ? MasterComputer.localChannel : channel).call(new Callable() { + store = (channel==null ? FilePath.localChannel : channel).call(new Callable() { public FilePath call() throws IOException { File home = new File(System.getProperty("user.home")); - return new FilePath(new File(home, ".hudson/cli-credentials")); + File hudsonHome = new File(home, ".hudson"); + if (hudsonHome.exists()) { + return new FilePath(new File(hudsonHome, "cli-credentials")); + } + return new FilePath(new File(home, ".jenkins/cli-credentials")); } }); if (store.exists()) { diff --git a/core/src/main/java/hudson/cli/ConsoleCommand.java b/core/src/main/java/hudson/cli/ConsoleCommand.java index b6edd0be80f6f4ffa931658bfdfaae7ea76e0a6e..6064cfd6ead8067580f29f5e3f816f3d4e8ee8ca 100644 --- a/core/src/main/java/hudson/cli/ConsoleCommand.java +++ b/core/src/main/java/hudson/cli/ConsoleCommand.java @@ -26,7 +26,7 @@ import java.io.PrintStream; public class ConsoleCommand extends CLICommand { @Override public String getShortDescription() { - return "Retrieves console output of a build"; + return Messages.ConsoleCommand_ShortDescription(); } @Argument(metaVar="JOB",usage="Name of the job",required=true) @@ -77,7 +77,7 @@ public class ConsoleCommand extends CLICommand { } else { InputStream in = run.getLogInputStream(); IOUtils.skip(in,pos); - IOUtils.copy(new InputStreamReader(in,run.getCharset()),w); + org.apache.commons.io.IOUtils.copy(new InputStreamReader(in,run.getCharset()),w); } } finally { w.flush(); // this pointless flush needed to work around SSHD-154 @@ -134,7 +134,7 @@ public class ConsoleCommand extends CLICommand { return rb.get(); } finally { - IOUtils.closeQuietly(in); + org.apache.commons.io.IOUtils.closeQuietly(in); } } diff --git a/core/src/main/java/hudson/cli/CopyJobCommand.java b/core/src/main/java/hudson/cli/CopyJobCommand.java index a8049bdf28cb4b5a320f5f3cd5921c64afd7e28a..7d83fa99a076ec00c7228c6e4b58b7f562d111ff 100644 --- a/core/src/main/java/hudson/cli/CopyJobCommand.java +++ b/core/src/main/java/hudson/cli/CopyJobCommand.java @@ -51,7 +51,6 @@ public class CopyJobCommand extends CLICommand { protected int run() throws Exception { Jenkins jenkins = Jenkins.getInstance(); - jenkins.checkPermission(Item.CREATE); if (jenkins.getItemByFullName(dst)!=null) { stderr.println("Job '"+dst+"' already exists"); @@ -75,7 +74,7 @@ public class CopyJobCommand extends CLICommand { dst = dst.substring(i + 1); } - ig.copy(src,dst); + ig.copy(src,dst).save(); return 0; } } diff --git a/core/src/main/java/hudson/cli/CreateJobCommand.java b/core/src/main/java/hudson/cli/CreateJobCommand.java index db73c30b51a03816d5a9288aab32b9d4ff3ca618..1e1b807c5ee257880a094a74c8d8bde7dc3db83d 100644 --- a/core/src/main/java/hudson/cli/CreateJobCommand.java +++ b/core/src/main/java/hudson/cli/CreateJobCommand.java @@ -23,8 +23,6 @@ */ package hudson.cli; -import hudson.model.ModifiableItemGroup; -import hudson.model.TopLevelItem; import jenkins.model.Jenkins; import hudson.Extension; import hudson.model.Item; @@ -48,7 +46,6 @@ public class CreateJobCommand extends CLICommand { protected int run() throws Exception { Jenkins h = Jenkins.getInstance(); - h.checkPermission(Item.CREATE); if (h.getItemByFullName(name)!=null) { stderr.println("Job '"+name+"' already exists"); @@ -72,6 +69,7 @@ public class CreateJobCommand extends CLICommand { name = name.substring(i + 1); } + Jenkins.checkGoodName(name); ig.createProjectFromXML(name, stdin); return 0; } diff --git a/core/src/main/java/hudson/cli/CreateViewCommand.java b/core/src/main/java/hudson/cli/CreateViewCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..73fd374b854195f0d96de49f582f61c364509b32 --- /dev/null +++ b/core/src/main/java/hudson/cli/CreateViewCommand.java @@ -0,0 +1,76 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import org.kohsuke.args4j.Argument; + +import jenkins.model.Jenkins; +import hudson.Extension; +import hudson.model.Failure; +import hudson.model.View; + +/** + * @author ogondza + * @since 1.538 + */ +@Extension +public class CreateViewCommand extends CLICommand { + + @Argument(usage="Name of the view to use instead of the one in XML") + public String viewName = null; + + @Override + public String getShortDescription() { + + return Messages.CreateViewCommand_ShortDescription(); + } + + @Override + protected int run() throws Exception { + + final Jenkins jenkins = Jenkins.getInstance(); + jenkins.checkPermission(View.CREATE); + + View newView; + try { + + newView = View.createViewFromXML(viewName, stdin); + } catch (Failure ex) { + + stderr.format("Invalid view name: %s\n", ex.getMessage()); + return -1; + } + + final String newName = newView.getViewName(); + if (jenkins.getView(newName) != null) { + + stderr.format("View '%s' already exists\n", newName); + return -1; + } + + jenkins.addView(newView); + + return 0; + } +} diff --git a/core/src/main/java/hudson/cli/DeleteViewCommand.java b/core/src/main/java/hudson/cli/DeleteViewCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..73414701139bab47cc931edb63df4b12e4e7b4c7 --- /dev/null +++ b/core/src/main/java/hudson/cli/DeleteViewCommand.java @@ -0,0 +1,67 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import hudson.Extension; +import hudson.model.ViewGroup; +import hudson.model.View; + +import org.kohsuke.args4j.Argument; + +/** + * @author ogondza + * @since 1.538 + */ +@Extension +public class DeleteViewCommand extends CLICommand { + + @Argument(usage="Name of the view to delete", required=true) + private View view; + + @Override + public String getShortDescription() { + + return Messages.DeleteViewCommand_ShortDescription(); + } + + @Override + protected int run() throws Exception { + + view.checkPermission(View.DELETE); + + final ViewGroup group = view.getOwner(); + if (!group.canDelete(view)) { + + stderr.format("%s does not allow to delete '%s' view\n", + group.getDisplayName(), + view.getViewName() + ); + return -1; + } + + group.deleteView(view);; + + return 0; + } +} diff --git a/core/src/main/java/hudson/cli/GetJobCommand.java b/core/src/main/java/hudson/cli/GetJobCommand.java index 0b8223442cb9ec78f2a5b9189254d126552f7c78..efc1e90836927f593b6016c53615634d20d67978 100644 --- a/core/src/main/java/hudson/cli/GetJobCommand.java +++ b/core/src/main/java/hudson/cli/GetJobCommand.java @@ -24,7 +24,7 @@ package hudson.cli; import hudson.Extension; -import hudson.model.AbstractProject; +import hudson.model.AbstractItem; import hudson.model.Item; import hudson.util.IOUtils; import org.kohsuke.args4j.Argument; @@ -35,7 +35,7 @@ import org.kohsuke.args4j.Argument; @Extension public class GetJobCommand extends CLICommand { @Argument(metaVar="JOB",usage="Name of the job",required=true) - public AbstractProject job; + public AbstractItem job; @Override public String getShortDescription() { diff --git a/core/src/main/java/hudson/cli/GetViewCommand.java b/core/src/main/java/hudson/cli/GetViewCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..0ae0ffa552af691e4cdef28be2526917bb291eda --- /dev/null +++ b/core/src/main/java/hudson/cli/GetViewCommand.java @@ -0,0 +1,55 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import hudson.Extension; +import hudson.model.View; + +import org.kohsuke.args4j.Argument; + +/** + * @author ogondza + * @since 1.538 + */ +@Extension +public class GetViewCommand extends CLICommand { + + @Argument(usage="Name of the view to obtain", required=true) + private View view; + + @Override + public String getShortDescription() { + + return Messages.GetViewCommand_ShortDescription(); + } + + @Override + protected int run() throws Exception { + + view.checkPermission(View.READ); + view.writeXml(stdout); + + return 0; + } +} diff --git a/core/src/main/java/hudson/cli/GroovyCommand.java b/core/src/main/java/hudson/cli/GroovyCommand.java index 7035bfdaf9591a6f0ec8a0784145a2a09ad4e474..7866e2f7228d6fde0ea480631c52676da97702fc 100644 --- a/core/src/main/java/hudson/cli/GroovyCommand.java +++ b/core/src/main/java/hudson/cli/GroovyCommand.java @@ -66,7 +66,7 @@ public class GroovyCommand extends CLICommand { /** * Remaining arguments. */ - @Argument(index=1) + @Argument(metaVar="ARGUMENTS", index=1, usage="Command line arguments to pass into script.") public List remaining = new ArrayList(); protected int run() throws Exception { diff --git a/core/src/main/java/hudson/cli/GroovyshCommand.java b/core/src/main/java/hudson/cli/GroovyshCommand.java index ffd0b1d243c6283d0882f77883e1b88e7b3ba417..48babcb90dbc98a2ca04941813357fec51505427 100644 --- a/core/src/main/java/hudson/cli/GroovyshCommand.java +++ b/core/src/main/java/hudson/cli/GroovyshCommand.java @@ -34,14 +34,15 @@ import org.codehaus.groovy.tools.shell.Shell; import org.codehaus.groovy.tools.shell.util.XmlCommandRegistrar; import java.util.List; -import java.util.Locale; import java.io.PrintStream; import java.io.InputStream; import java.io.BufferedInputStream; import java.io.PrintWriter; +import java.util.ArrayList; import jline.UnsupportedTerminal; import jline.Terminal; +import org.kohsuke.args4j.Argument; /** * Executes Groovy shell. @@ -55,12 +56,12 @@ public class GroovyshCommand extends CLICommand { return Messages.GroovyshCommand_ShortDescription(); } + @Argument(metaVar="ARGS") public List args = new ArrayList(); + @Override - public int main(List args, Locale locale, InputStream stdin, PrintStream stdout, PrintStream stderr) { + protected int run() { // this allows the caller to manipulate the JVM state, so require the admin privilege. Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); - // TODO: ^as this class overrides main() (which has authentication stuff), - // how to get ADMIN permission for this command? // this being remote means no jline capability is available System.setProperty("jline.terminal", UnsupportedTerminal.class.getName()); @@ -123,7 +124,4 @@ public class GroovyshCommand extends CLICommand { return shell; } - protected int run() { - throw new UnsupportedOperationException(); - } } diff --git a/core/src/main/java/hudson/cli/HelpCommand.java b/core/src/main/java/hudson/cli/HelpCommand.java index d49dd2bccbef7920a9387ae27969ff93fee32fe3..bebb39dfcd9ce8aff1f1fb1a42c685e9ce941899 100644 --- a/core/src/main/java/hudson/cli/HelpCommand.java +++ b/core/src/main/java/hudson/cli/HelpCommand.java @@ -29,6 +29,8 @@ import jenkins.model.Jenkins; import java.util.Map; import java.util.TreeMap; +import org.kohsuke.args4j.Argument; + /** * Show the list of all commands. * @@ -36,18 +38,32 @@ import java.util.TreeMap; */ @Extension public class HelpCommand extends CLICommand { + + @Argument(metaVar="COMMAND", usage="Name of the command") + public String command; + @Override public String getShortDescription() { return Messages.HelpCommand_ShortDescription(); } + @Override protected int run() { if (!Jenkins.getInstance().hasPermission(Jenkins.READ)) { stderr.println("You must authenticate to access this Jenkins.\n" + "Use --username/--password/--password-file parameters or login command."); - return 0; + return -1; } + if (command != null) + return showCommandDetails(); + + showAllCommands(); + + return 0; + } + + private int showAllCommands() { Map commands = new TreeMap(); for (CLICommand c : CLICommand.all()) commands.put(c.getName(),c); @@ -56,6 +72,19 @@ public class HelpCommand extends CLICommand { stderr.println(" "+c.getName()); stderr.println(" "+c.getShortDescription()); } + + return 0; + } + + private int showCommandDetails() { + CLICommand command = CLICommand.clone(this.command); + if (command == null) { + stderr.format("No such command %s. Awailable commands are: ", this.command); + showAllCommands(); + return -1; + } + + command.printUsage(stderr, command.getCmdLineParser()); return 0; } diff --git a/core/src/main/java/hudson/cli/InstallPluginCommand.java b/core/src/main/java/hudson/cli/InstallPluginCommand.java index eb64582b083ffeba77e6a092ea1dbaf723ccb5ed..1fd23d81acba7de5c3a4050e4925c96b797934b9 100644 --- a/core/src/main/java/hudson/cli/InstallPluginCommand.java +++ b/core/src/main/java/hudson/cli/InstallPluginCommand.java @@ -26,7 +26,6 @@ package hudson.cli; import hudson.Extension; import hudson.FilePath; import hudson.PluginManager; -import hudson.util.IOException2; import jenkins.model.Jenkins; import hudson.model.UpdateSite; import hudson.model.UpdateSite.Data; @@ -35,6 +34,7 @@ import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; import java.io.File; +import java.io.IOException; import java.net.URL; import java.net.MalformedURLException; import java.util.HashSet; @@ -114,7 +114,7 @@ public class InstallPluginCommand extends CLICommand { stdout.println(Messages.InstallPluginCommand_InstallingFromUpdateCenter(source)); Throwable e = p.deploy(dynamicLoad).get().getError(); if (e!=null) - throw new IOException2("Failed to install plugin "+source,e); + throw new IOException("Failed to install plugin "+source,e); continue; } @@ -141,7 +141,7 @@ public class InstallPluginCommand extends CLICommand { } if (restart) - h.restart(); + h.safeRestart(); return 0; // all success } diff --git a/core/src/main/java/hudson/cli/InstallToolCommand.java b/core/src/main/java/hudson/cli/InstallToolCommand.java index fb30efd0c08b52798e618e95f1dce303aefca908..f5ad164f95e2a6dcbf5ad0e9d49160fce2f5fb3e 100644 --- a/core/src/main/java/hudson/cli/InstallToolCommand.java +++ b/core/src/main/java/hudson/cli/InstallToolCommand.java @@ -122,6 +122,9 @@ public class InstallToolCommand extends CLICommand { throw new AbortException(b.getFullDisplayName()+" is not building"); Node node = exec.getOwner().getNode(); + if (node == null) { + throw new AbortException("The node " + exec.getOwner().getDisplayName() + " has been deleted"); + } t = t.translate(node, EnvVars.getRemote(checkChannel()), new StreamTaskListener(stderr)); stdout.println(t.getHome()); diff --git a/core/src/main/java/hudson/cli/ListJobsCommand.java b/core/src/main/java/hudson/cli/ListJobsCommand.java index af858d3f04e6d3de3d6ca97a454685188d4e5467..bdb64208d5c827c71968be3ada50902960a28a78 100644 --- a/core/src/main/java/hudson/cli/ListJobsCommand.java +++ b/core/src/main/java/hudson/cli/ListJobsCommand.java @@ -24,16 +24,13 @@ package hudson.cli; import java.util.Collection; -import java.util.LinkedHashSet; - -import java.util.Collections; import hudson.model.Item; -import hudson.model.ItemGroup; +import hudson.model.Items; import hudson.model.TopLevelItem; -import hudson.model.ViewGroup; import hudson.model.View; import hudson.Extension; +import jenkins.model.ModifiableTopLevelItemGroup; import jenkins.model.Jenkins; import org.kohsuke.args4j.Argument; @@ -68,9 +65,8 @@ public class ListJobsCommand extends CLICommand { final Item item = h.getItemByFullName(name); // If item group was found use it's jobs. - if (item instanceof ItemGroup) { - ItemGroup itemGroup = (ItemGroup) item; - jobs = itemGroup.getItems(); + if (item instanceof ModifiableTopLevelItemGroup) { + jobs = Items.getAllItems((ModifiableTopLevelItemGroup) item, TopLevelItem.class); } // No view and no item group with the given name found. else { diff --git a/core/src/main/java/hudson/cli/RemoveJobFromViewCommand.java b/core/src/main/java/hudson/cli/RemoveJobFromViewCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..8c317d908f97bccfe39c14f8434a2167e1814249 --- /dev/null +++ b/core/src/main/java/hudson/cli/RemoveJobFromViewCommand.java @@ -0,0 +1,68 @@ +/* + * The MIT License + * + * Copyright (c) 2014 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import java.util.List; + +import hudson.Extension; +import hudson.model.TopLevelItem; +import hudson.model.DirectlyModifiableView; +import hudson.model.View; + +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.CmdLineException; + +/** + * @author ogondza + * @since 1.570 + */ +@Extension +public class RemoveJobFromViewCommand extends CLICommand { + + @Argument(usage="Name of the view", required=true, index=0) + private View view; + + @Argument(usage="Job names", required=true, index=1) + private List jobs; + + @Override + public String getShortDescription() { + return Messages.RemoveJobFromViewCommand_ShortDescription(); + } + + @Override + protected int run() throws Exception { + view.checkPermission(View.CONFIGURE); + + if (!(view instanceof DirectlyModifiableView)) throw new CmdLineException( + null, "'" + view.getDisplayName() + "' view can not be modified directly" + ); + + for (TopLevelItem job: jobs) { + ((DirectlyModifiableView) view).remove(job); + } + + return 0; + } +} diff --git a/core/src/main/java/hudson/cli/SessionIdCommand.java b/core/src/main/java/hudson/cli/SessionIdCommand.java index 5a38f489228d42435a8c2358ca28e54c36f5d100..5e45dab55d57a678f83d3e7f6f6a8edb08966549 100644 --- a/core/src/main/java/hudson/cli/SessionIdCommand.java +++ b/core/src/main/java/hudson/cli/SessionIdCommand.java @@ -13,7 +13,7 @@ import jenkins.model.Jenkins; public class SessionIdCommand extends CLICommand { @Override public String getShortDescription() { - return "Outputs the session ID, which changes every time Jenkins restarts"; + return Messages.SessionIdCommand_ShortDescription(); } protected int run() { diff --git a/core/src/main/java/hudson/cli/SetBuildDisplayNameCommand.java b/core/src/main/java/hudson/cli/SetBuildDisplayNameCommand.java index 5233cf7f7f5f408a685bb93bf1fb61f7a11befbb..08b08b7d7166b510e545d598cfddbad85c299ad7 100644 --- a/core/src/main/java/hudson/cli/SetBuildDisplayNameCommand.java +++ b/core/src/main/java/hudson/cli/SetBuildDisplayNameCommand.java @@ -3,15 +3,14 @@ package hudson.cli; import hudson.Extension; import hudson.model.AbstractProject; import hudson.model.Run; -import hudson.remoting.Callable; import org.apache.commons.io.IOUtils; import org.kohsuke.args4j.Argument; -import java.io.IOException; import java.io.Serializable; @Extension public class SetBuildDisplayNameCommand extends CLICommand implements Serializable { + private static final long serialVersionUID = 6665171784136358536L; @Override public String getShortDescription() { @@ -27,8 +26,13 @@ public class SetBuildDisplayNameCommand extends CLICommand implements Serializab @Argument(metaVar="DISPLAYNAME", required=true, usage="DisplayName to be set. '-' to read from stdin.", index=2) public String displayName; + @Override protected int run() throws Exception { - Run run = job.getBuildByNumber(number); + Run run = job.getBuildByNumber(number); + if (run == null) { + stderr.format("Build #%d does not exist\n", number); + return -1; + } run.checkPermission(Run.UPDATE); if ("-".equals(displayName)) { @@ -39,5 +43,4 @@ public class SetBuildDisplayNameCommand extends CLICommand implements Serializab return 0; } - } diff --git a/core/src/main/java/hudson/cli/SetBuildParameterCommand.java b/core/src/main/java/hudson/cli/SetBuildParameterCommand.java index 2862e9e8d79f8e6c65da69f4f7cb917f26a98aae..5676bb52643d01b2ebb12f03c131dfe019c1e2b6 100644 --- a/core/src/main/java/hudson/cli/SetBuildParameterCommand.java +++ b/core/src/main/java/hudson/cli/SetBuildParameterCommand.java @@ -18,10 +18,10 @@ import java.util.Collections; */ @Extension public class SetBuildParameterCommand extends CommandDuringBuild { - @Argument(index=0, required=true, usage="Name of the build variable") + @Argument(index=0, metaVar="NAME", required=true, usage="Name of the build variable") public String name; - @Argument(index=1,required=true, usage="Value of the build variable") + @Argument(index=1, metaVar="VALUE", required=true, usage="Value of the build variable") public String value; @Override @@ -37,9 +37,7 @@ public class SetBuildParameterCommand extends CommandDuringBuild { ParametersAction a = r.getAction(ParametersAction.class); if (a!=null) { - ParametersAction b = a.createUpdated(Collections.singleton(p)); - r.addAction(b); - r.getActions().remove(a); + r.replaceAction(a.createUpdated(Collections.singleton(p))); } else { r.addAction(new ParametersAction(p)); } diff --git a/core/src/main/java/hudson/cli/UpdateJobCommand.java b/core/src/main/java/hudson/cli/UpdateJobCommand.java index eaea4227725943861748e87bae1f133bb5651503..a9a34403ec03699296d900e103a498efdccfa008 100644 --- a/core/src/main/java/hudson/cli/UpdateJobCommand.java +++ b/core/src/main/java/hudson/cli/UpdateJobCommand.java @@ -24,7 +24,7 @@ package hudson.cli; import hudson.Extension; -import hudson.model.AbstractProject; +import hudson.model.AbstractItem; import org.kohsuke.args4j.Argument; import javax.xml.transform.Source; @@ -36,7 +36,7 @@ import javax.xml.transform.stream.StreamSource; @Extension public class UpdateJobCommand extends CLICommand { @Argument(metaVar="JOB",usage="Name of the job",required=true) - public AbstractProject job; + public AbstractItem job; @Override public String getShortDescription() { diff --git a/core/src/main/java/hudson/cli/UpdateViewCommand.java b/core/src/main/java/hudson/cli/UpdateViewCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..4f861fcb04d991d1d3a7ea49f3180994878cebdb --- /dev/null +++ b/core/src/main/java/hudson/cli/UpdateViewCommand.java @@ -0,0 +1,55 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli; + +import javax.xml.transform.stream.StreamSource; + +import hudson.Extension; +import hudson.model.View; + +import org.kohsuke.args4j.Argument; + +/** + * @author ogondza + * @since 1.538 + */ +@Extension +public class UpdateViewCommand extends CLICommand { + + @Argument(usage="Name of the view to update", required=true) + private View view; + + @Override + public String getShortDescription() { + + return Messages.UpdateViewCommand_ShortDescription(); + } + + @Override + protected int run() throws Exception { + + view.updateByXml(new StreamSource(stdin)); + return 0; + } +} diff --git a/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java b/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java index 9cc5448616d51fb0ead5aa7e9edd41449099bacd..cb1d1db8f2386c23f3f10a8c7b7cd2ee2e97de57 100644 --- a/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java +++ b/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java @@ -110,48 +110,65 @@ public class CLIRegisterer extends ExtensionFinder { return name; } + @Override public String getShortDescription() { // format by using the right locale return res.format("CLI."+name+".shortDescription"); } + @Override + protected CmdLineParser getCmdLineParser() { + return bindMethod(new ArrayList()); + } + + private CmdLineParser bindMethod(List binders) { + + registerOptionHandlers(); + CmdLineParser parser = new CmdLineParser(null); + + // build up the call sequence + Stack chains = new Stack(); + Method method = m; + while (true) { + chains.push(method); + if (Modifier.isStatic(method.getModifiers())) + break; // the chain is complete. + + // the method in question is an instance method, so we need to resolve the instance by using another resolver + Class type = method.getDeclaringClass(); + try { + method = findResolver(type); + } catch (IOException ex) { + throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for "+type, ex); + } + if (method==null) { + throw new RuntimeException("Unable to find the resolver method annotated with @CLIResolver for "+type); + } + } + + while (!chains.isEmpty()) + binders.add(new MethodBinder(chains.pop(),this,parser)); + + new ClassParser().parse(Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this), parser); + + return parser; + } + @Override public int main(List args, Locale locale, InputStream stdin, PrintStream stdout, PrintStream stderr) { this.stdout = stdout; this.stderr = stderr; this.locale = locale; - registerOptionHandlers(); - CmdLineParser parser = new CmdLineParser(null); + List binders = new ArrayList(); + + CmdLineParser parser = bindMethod(binders); try { SecurityContext sc = SecurityContextHolder.getContext(); Authentication old = sc.getAuthentication(); try { - // build up the call sequence - Stack chains = new Stack(); - Method method = m; - while (true) { - chains.push(method); - if (Modifier.isStatic(method.getModifiers())) - break; // the chain is complete. - - // the method in question is an instance method, so we need to resolve the instance by using another resolver - Class type = method.getDeclaringClass(); - method = findResolver(type); - if (method==null) { - stderr.println("Unable to find the resolver method annotated with @CLIResolver for "+type); - return 1; - } - } - - List binders = new ArrayList(); - - while (!chains.isEmpty()) - binders.add(new MethodBinder(chains.pop(),this,parser)); - // authentication CliAuthenticator authenticator = Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this); - new ClassParser().parse(authenticator,parser); // fill up all the binders parser.parseArgument(args); diff --git a/core/src/main/java/hudson/cli/declarative/OptionHandlerExtension.java b/core/src/main/java/hudson/cli/declarative/OptionHandlerExtension.java index b7a18e305b64c447bc58935e993a530708fce693..7e76e117d2b6260bbdd078f1bcaa1f5d193ac84a 100644 --- a/core/src/main/java/hudson/cli/declarative/OptionHandlerExtension.java +++ b/core/src/main/java/hudson/cli/declarative/OptionHandlerExtension.java @@ -36,7 +36,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * {@link OptionHandler}s that should be auto-discovered. - * + * TODO is this actually necessary? {@code @MetaInfServices(OptionHandler.class)} seems to work as well. * @author Kohsuke Kawaguchi */ @Indexed diff --git a/core/src/main/java/hudson/cli/handlers/AbstractItemOptionHandler.java b/core/src/main/java/hudson/cli/handlers/AbstractItemOptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..c99f3090799e291487bff797f9f2b457c26bfb84 --- /dev/null +++ b/core/src/main/java/hudson/cli/handlers/AbstractItemOptionHandler.java @@ -0,0 +1,48 @@ +/* + * The MIT License + * + * Copyright 2013 Jesse Glick. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson.cli.handlers; + +import hudson.model.AbstractItem; +import org.kohsuke.MetaInfServices; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.OptionDef; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.Setter; + +/** + * Refers to an {@link AbstractItem} by name. + * @since 1.538 + */ +@MetaInfServices(OptionHandler.class) public class AbstractItemOptionHandler extends GenericItemOptionHandler { + + public AbstractItemOptionHandler(CmdLineParser parser, OptionDef option, Setter setter) { + super(parser, option, setter); + } + + @Override protected Class type() { + return AbstractItem.class; + } + +} diff --git a/core/src/main/java/hudson/cli/handlers/AbstractProjectOptionHandler.java b/core/src/main/java/hudson/cli/handlers/AbstractProjectOptionHandler.java index 0dc515751ca8e16b7c78d92c8327dd7ff0c0d83e..b630e48b56abe656345304b8a7f7acc339c1cfe2 100644 --- a/core/src/main/java/hudson/cli/handlers/AbstractProjectOptionHandler.java +++ b/core/src/main/java/hudson/cli/handlers/AbstractProjectOptionHandler.java @@ -24,42 +24,26 @@ package hudson.cli.handlers; import hudson.model.AbstractProject; -import jenkins.model.Jenkins; -import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.OptionDef; -import org.kohsuke.args4j.spi.OptionHandler; -import org.kohsuke.args4j.spi.Parameters; import org.kohsuke.args4j.spi.Setter; import org.kohsuke.MetaInfServices; +import org.kohsuke.args4j.spi.OptionHandler; /** * Refer to {@link AbstractProject} by its name. * * @author Kohsuke Kawaguchi */ -@MetaInfServices +@MetaInfServices(OptionHandler.class) @SuppressWarnings("rawtypes") -public class AbstractProjectOptionHandler extends OptionHandler { +public class AbstractProjectOptionHandler extends GenericItemOptionHandler { public AbstractProjectOptionHandler(CmdLineParser parser, OptionDef option, Setter setter) { super(parser, option, setter); } - @Override - public int parseArguments(Parameters params) throws CmdLineException { - Jenkins h = Jenkins.getInstance(); - String src = params.getParameter(0); - - AbstractProject s = h.getItemByFullName(src,AbstractProject.class); - if (s==null) { - AbstractProject nearest = AbstractProject.findNearest(src); - if (nearest!=null) - throw new CmdLineException(owner, "No such job '"+src+"' perhaps you meant '"+ nearest.getFullName() +"'?"); - else - throw new CmdLineException(owner, "No such job '"+src+"'"); - } - setter.addValue(s); - return 1; + @Override protected Class type() { + return AbstractProject.class; } @Override diff --git a/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java b/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..e27bb32dc8709b8972c64cd7db80b24b0c61634f --- /dev/null +++ b/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java @@ -0,0 +1,89 @@ +/* + * The MIT License + * + * Copyright 2013 Jesse Glick. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson.cli.handlers; + +import hudson.model.Item; +import hudson.model.Items; +import hudson.security.ACL; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import org.acegisecurity.Authentication; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.OptionDef; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.Parameters; +import org.kohsuke.args4j.spi.Setter; + +/** + * Refers to an {@link Item} by its name. + * May be subclassed to handle specific kinds of items. + * (Use {@code @MetaInfServices(OptionHandler.class)} to register the subclass.) + * @param the kind of item being handled + * @since 1.538 + */ +public abstract class GenericItemOptionHandler extends OptionHandler { + + private static final Logger LOGGER = Logger.getLogger(GenericItemOptionHandler.class.getName()); + + protected GenericItemOptionHandler(CmdLineParser parser, OptionDef option, Setter setter) { + super(parser, option, setter); + } + + protected abstract Class type(); + + @Override public int parseArguments(Parameters params) throws CmdLineException { + final Jenkins j = Jenkins.getInstance(); + final String src = params.getParameter(0); + T s = j.getItemByFullName(src, type()); + if (s == null) { + final Authentication who = Jenkins.getAuthentication(); + ACL.impersonate(ACL.SYSTEM, new Runnable() { + @Override public void run() { + Item actual = j.getItemByFullName(src); + if (actual == null) { + LOGGER.log(Level.FINE, "really no item exists named {0}", src); + } else { + LOGGER.log(Level.WARNING, "running as {0} could not find {1} of {2}", new Object[] {who.getPrincipal(), actual, type()}); + } + } + }); + T nearest = Items.findNearest(type(), src, j); + if (nearest != null) { + throw new CmdLineException(owner, "No such job '" + src + "'; perhaps you meant '" + nearest.getFullName() + "'?"); + } else { + throw new CmdLineException(owner, "No such job '" + src + "'"); + } + } + setter.addValue(s); + return 1; + } + + @Override public String getDefaultMetaVariable() { + return "ITEM"; + } + +} diff --git a/core/src/main/java/hudson/cli/handlers/TopLevelItemOptionHandler.java b/core/src/main/java/hudson/cli/handlers/TopLevelItemOptionHandler.java index 0c4a4ea97869db3f6976869405256a328c0b128e..97fdf4b8c5b648585af94cb233a515529462fb3d 100644 --- a/core/src/main/java/hudson/cli/handlers/TopLevelItemOptionHandler.java +++ b/core/src/main/java/hudson/cli/handlers/TopLevelItemOptionHandler.java @@ -1,14 +1,10 @@ package hudson.cli.handlers; -import hudson.model.AbstractProject; -import jenkins.model.Jenkins; import hudson.model.TopLevelItem; import org.kohsuke.MetaInfServices; -import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.OptionDef; import org.kohsuke.args4j.spi.OptionHandler; -import org.kohsuke.args4j.spi.Parameters; import org.kohsuke.args4j.spi.Setter; /** @@ -16,33 +12,18 @@ import org.kohsuke.args4j.spi.Setter; * * @author Kohsuke Kawaguchi */ -@MetaInfServices -public class TopLevelItemOptionHandler extends OptionHandler { +@MetaInfServices(OptionHandler.class) +public class TopLevelItemOptionHandler extends GenericItemOptionHandler { public TopLevelItemOptionHandler(CmdLineParser parser, OptionDef option, Setter setter) { super(parser, option, setter); } - @Override - @SuppressWarnings("rawtypes") - public int parseArguments(Parameters params) throws CmdLineException { - Jenkins h = Jenkins.getInstance(); - String src = params.getParameter(0); - - TopLevelItem s = h.getItemByFullName(src, TopLevelItem.class); - if (s==null) { - AbstractProject nearest = AbstractProject.findNearest(src); - if (nearest!=null) - throw new CmdLineException(owner, "No such job '"+src+"' perhaps you meant '"+ nearest.getFullName() +"'?"); - else - throw new CmdLineException(owner, "No such job '"+src+"'"); - } - - setter.addValue(s); - return 1; + @Override protected Class type() { + return TopLevelItem.class; } @Override public String getDefaultMetaVariable() { - return "JOB"; + return "JOB"; // TODO or should we pick up default value, ITEM? } } diff --git a/core/src/main/java/hudson/cli/handlers/ViewOptionHandler.java b/core/src/main/java/hudson/cli/handlers/ViewOptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..387621ae56b570e70731cbf7f6a6f6a98e755d41 --- /dev/null +++ b/core/src/main/java/hudson/cli/handlers/ViewOptionHandler.java @@ -0,0 +1,111 @@ +/* + * The MIT License + * + * Copyright (c) 2013, Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.cli.handlers; + +import hudson.model.ViewGroup; +import hudson.model.View; + +import java.util.StringTokenizer; + +import jenkins.model.Jenkins; + +import org.kohsuke.MetaInfServices; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.OptionDef; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.Parameters; +import org.kohsuke.args4j.spi.Setter; + +/** + * Refers to {@link View} by its name. + * + *

+ * For example: + *

+ *
my_view_name
refers to a top level view with given name.
+ *
nested/inner
refers to a view named inner inside of a top level view group named nested.
+ *
+ * + *

+ * View name is a non-empty sequence of {@link View} names delimited by '/'. + * Handler traverse the view names from left to right. First one is expected to + * be a top level view and all but the last one are expected to be instances of + * {@link ViewGroup}. Handler fails to resolve view provided a view with given + * name does not exist or a user was not granted {@link View#READ} permission. + * + * @author ogondza + * @since 1.538 + */ +@MetaInfServices +public class ViewOptionHandler extends OptionHandler { + + public ViewOptionHandler(CmdLineParser parser, OptionDef option, Setter setter) { + + super(parser, option, setter); + } + + @Override + public int parseArguments(Parameters params) throws CmdLineException { + + setter.addValue(getView(params.getParameter(0))); + return 1; + } + + private View getView(String name) throws CmdLineException { + + View view = null; + ViewGroup group = Jenkins.getInstance(); + + final StringTokenizer tok = new StringTokenizer(name, "/"); + while(tok.hasMoreTokens()) { + + String viewName = tok.nextToken(); + + view = group.getView(viewName); + if (view == null) throw new CmdLineException(owner, String.format( + "No view named %s inside view %s", + viewName, group.getDisplayName() + )); + + view.checkPermission(View.READ); + + if (view instanceof ViewGroup) { + group = (ViewGroup) view; + } else if (tok.hasMoreTokens()) { + throw new CmdLineException( + owner, view.getViewName() + " view can not contain views" + ); + } + } + + return view; + } + + @Override + public String getDefaultMetaVariable() { + + return "VIEW"; + } +} diff --git a/core/src/main/java/hudson/console/AnnotatedLargeText.java b/core/src/main/java/hudson/console/AnnotatedLargeText.java index 298e034bf15ebf31a8f728b5c6bb9e7a6a82946b..4a3af765bdd04dd2831a451db7ccb5cd4d45c631 100644 --- a/core/src/main/java/hudson/console/AnnotatedLargeText.java +++ b/core/src/main/java/hudson/console/AnnotatedLargeText.java @@ -28,8 +28,6 @@ package hudson.console; import com.trilead.ssh2.crypto.Base64; import jenkins.model.Jenkins; import hudson.remoting.ObjectInputStreamEx; -import hudson.util.IOException2; -import hudson.util.Secret; import hudson.util.TimeUnit2; import jenkins.security.CryptoConfidentialKey; import org.apache.commons.io.output.ByteArrayOutputStream; @@ -50,7 +48,6 @@ import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Writer; import java.nio.charset.Charset; -import java.security.GeneralSecurityException; import com.jcraft.jzlib.GZIPInputStream; import com.jcraft.jzlib.GZIPOutputStream; @@ -134,7 +131,7 @@ public class AnnotatedLargeText extends LargeText { } } } catch (ClassNotFoundException e) { - throw new IOException2(e); + throw new IOException(e); } // start from scratch return ConsoleAnnotator.initial(context==null ? null : context.getClass()); diff --git a/core/src/main/java/hudson/console/ConsoleNote.java b/core/src/main/java/hudson/console/ConsoleNote.java index c8ea004643309d73af2d5638bab3bf3f225d50d2..c27fec63643527d8b9f775f6e1c6b6a739a47f62 100644 --- a/core/src/main/java/hudson/console/ConsoleNote.java +++ b/core/src/main/java/hudson/console/ConsoleNote.java @@ -30,7 +30,6 @@ import hudson.model.Describable; import jenkins.model.Jenkins; import hudson.model.Run; import hudson.remoting.ObjectInputStreamEx; -import hudson.util.IOException2; import hudson.util.IOUtils; import hudson.util.UnbufferedBase64InputStream; import org.apache.commons.codec.binary.Base64OutputStream; @@ -172,16 +171,22 @@ public abstract class ConsoleNote implements Serializable, Describable implements Serializable, Describable { * In addition, the last character shouldn't be ',' ':', '"', etc, as often those things show up right next * to URL in plain text (e.g., test="http://www.example.com/") */ - private static final Pattern URL = Pattern.compile("\\b(http|https|ftp)://[^\\s<>]+[^\\s<>,\\.:\"'()\\[\\]=]"); + private static final Pattern URL = Pattern.compile("\\b(http|https|file|ftp)://[^\\s<>]+[^\\s<>,\\.:\"'()\\[\\]=]"); private static final String OPEN = "'\"()[]<>"; private static final String CLOSE= "'\")(][><"; diff --git a/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageChecker.java b/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageChecker.java index 3576c4388de7cd6e2c61a82b79dd7467322dcbbf..5cf0c81ee412aca2174c44a2e878daa73ed6f4f4 100644 --- a/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageChecker.java +++ b/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageChecker.java @@ -26,7 +26,6 @@ package hudson.diagnosis; import hudson.Extension; import jenkins.model.Jenkins; import hudson.model.PeriodicWork; -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import java.util.logging.Logger; @@ -42,9 +41,7 @@ public class HudsonHomeDiskUsageChecker extends PeriodicWork { return HOUR; } - @IgnoreJRERequirement protected void doRun() { - try { long free = Jenkins.getInstance().getRootDir().getUsableSpace(); long total = Jenkins.getInstance().getRootDir().getTotalSpace(); if(free<=0 || total<=0) { @@ -61,11 +58,6 @@ public class HudsonHomeDiskUsageChecker extends PeriodicWork { // it's AND and not OR so that small Hudson home won't get a warning, // and similarly, if you have a 1TB disk, you don't get a warning when you still have 100GB to go. HudsonHomeDiskUsageMonitor.get().activated = (total/free>10 && free< FREE_SPACE_THRESHOLD); - } catch (LinkageError _) { - // pre Mustang - LOGGER.info("Not on JDK6. Cannot monitor JENKINS_HOME disk usage"); - cancel(); - } } private static final Logger LOGGER = Logger.getLogger(HudsonHomeDiskUsageChecker.class.getName()); diff --git a/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageMonitor.java b/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageMonitor.java index a72bcd68b3cb9f62e4b88dfebd1571d1ad6810b3..b37934f0232790b4a36f34567bb8bd9c0de294e9 100644 --- a/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageMonitor.java +++ b/core/src/main/java/hudson/diagnosis/HudsonHomeDiskUsageMonitor.java @@ -55,6 +55,11 @@ public final class HudsonHomeDiskUsageMonitor extends AdministrativeMonitor { public boolean isActivated() { return activated; } + + @Override + public String getDisplayName() { + return Messages.HudsonHomeDiskUsageMonitor_DisplayName(); + } /** * Depending on whether the user said "yes" or "no", send him to the right place. diff --git a/core/src/main/java/hudson/diagnosis/OldDataMonitor.java b/core/src/main/java/hudson/diagnosis/OldDataMonitor.java index 4b1a6bebaf628803ec4361d0601f3249832e4eca..b9e18d1b689d04f7d376293b7c0110c16019c81f 100644 --- a/core/src/main/java/hudson/diagnosis/OldDataMonitor.java +++ b/core/src/main/java/hudson/diagnosis/OldDataMonitor.java @@ -23,13 +23,13 @@ */ package hudson.diagnosis; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import hudson.Extension; import hudson.XmlFile; import hudson.model.AdministrativeMonitor; -import hudson.model.ManagementLink; -import jenkins.model.Jenkins; -import hudson.Extension; import hudson.model.Item; import hudson.model.Job; +import hudson.model.ManagementLink; import hudson.model.Run; import hudson.model.Saveable; import hudson.model.listeners.ItemListener; @@ -37,22 +37,22 @@ import hudson.model.listeners.RunListener; import hudson.model.listeners.SaveableListener; import hudson.util.RobustReflectionConverter; import hudson.util.VersionNumber; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; -import com.thoughtworks.xstream.converters.UnmarshallingContext; - import java.io.IOException; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.CheckForNull; +import jenkins.model.Jenkins; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.interceptor.RequirePOST; /** * Tracks whether any data structure changes were corrected when loading XML, @@ -62,11 +62,15 @@ import org.kohsuke.stapler.HttpResponses; */ @Extension public class OldDataMonitor extends AdministrativeMonitor { - private static Logger LOGGER = Logger.getLogger(OldDataMonitor.class.getName()); + private static final Logger LOGGER = Logger.getLogger(OldDataMonitor.class.getName()); - private HashMap data = new HashMap(); + private HashMap data = new HashMap(); private boolean updating = false; + static OldDataMonitor get(Jenkins j) { + return (OldDataMonitor) j.getAdministrativeMonitor("OldData"); + } + public OldDataMonitor() { super("OldData"); } @@ -81,17 +85,24 @@ public class OldDataMonitor extends AdministrativeMonitor { } public synchronized Map getData() { - return Collections.unmodifiableMap(data); + Map r = new HashMap(); + for (Map.Entry entry : data.entrySet()) { + Saveable s = entry.getKey().get(); + if (s != null) { + r.put(s, entry.getValue()); + } + } + return r; } private static void remove(Saveable obj, boolean isDelete) { - OldDataMonitor odm = (OldDataMonitor) Jenkins.getInstance().getAdministrativeMonitor("OldData"); + OldDataMonitor odm = get(Jenkins.getInstance()); synchronized (odm) { if (odm.updating) return; // Skip during doUpgrade or doDiscard - odm.data.remove(obj); + odm.data.remove(referTo(obj)); if (isDelete && obj instanceof Job) for (Run r : ((Job)obj).getBuilds()) - odm.data.remove(r); + odm.data.remove(referTo(r)); } } @@ -129,12 +140,13 @@ public class OldDataMonitor extends AdministrativeMonitor { * @param version Hudson release when the data structure changed. */ public static void report(Saveable obj, String version) { - OldDataMonitor odm = (OldDataMonitor) Jenkins.getInstance().getAdministrativeMonitor("OldData"); + OldDataMonitor odm = get(Jenkins.getInstance()); synchronized (odm) { try { - VersionRange vr = odm.data.get(obj); + SaveableReference ref = referTo(obj); + VersionRange vr = odm.data.get(ref); if (vr != null) vr.add(version); - else odm.data.put(obj, new VersionRange(version, null)); + else odm.data.put(ref, new VersionRange(version, null)); } catch (IllegalArgumentException ex) { LOGGER.log(Level.WARNING, "Bad parameter given to OldDataMonitor", ex); } @@ -175,11 +187,20 @@ public class OldDataMonitor extends AdministrativeMonitor { } } if (buf.length() == 0) return; - OldDataMonitor odm = (OldDataMonitor) Jenkins.getInstance().getAdministrativeMonitor("OldData"); + Jenkins j = Jenkins.getInstance(); + if (j == null) { + // Startup failed, something is very broken, so report what we can. + for (Throwable t : errors) { + LOGGER.log(Level.WARNING, "could not read " + obj + " (and Jenkins did not start up)", t); + } + return; + } + OldDataMonitor odm = get(j); synchronized (odm) { - VersionRange vr = odm.data.get(obj); + SaveableReference ref = referTo(obj); + VersionRange vr = odm.data.get(ref); if (vr != null) vr.extra = buf.toString(); - else odm.data.put(obj, new VersionRange(null, buf.toString())); + else odm.data.put(ref, new VersionRange(null, buf.toString())); } } @@ -234,6 +255,7 @@ public class OldDataMonitor extends AdministrativeMonitor { /** * Depending on whether the user said "yes" or "no", send him to the right place. */ + @RequirePOST public HttpResponse doAct(StaplerRequest req, StaplerResponse rsp) throws IOException { if (req.hasParameter("no")) { disable(true); @@ -247,16 +269,17 @@ public class OldDataMonitor extends AdministrativeMonitor { * Save all or some of the files to persist data in the new forms. * Remove those items from the data map. */ - public synchronized HttpResponse doUpgrade(StaplerRequest req, StaplerResponse rsp) throws IOException { + @RequirePOST + public synchronized HttpResponse doUpgrade(StaplerRequest req, StaplerResponse rsp) { String thruVerParam = req.getParameter("thruVer"); VersionNumber thruVer = thruVerParam.equals("all") ? null : new VersionNumber(thruVerParam); updating = true; - for (Iterator> it = data.entrySet().iterator(); it.hasNext();) { - Map.Entry entry = it.next(); + for (Iterator> it = data.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); VersionNumber version = entry.getValue().max; if (version != null && (thruVer == null || !version.isNewerThan(thruVer))) { - entry.getKey().save(); it.remove(); + tryToSave(entry.getKey()); } } updating = false; @@ -267,23 +290,89 @@ public class OldDataMonitor extends AdministrativeMonitor { * Save all files containing only unreadable data (no data upgrades), which discards this data. * Remove those items from the data map. */ - public synchronized HttpResponse doDiscard(StaplerRequest req, StaplerResponse rsp) throws IOException { + @RequirePOST + public synchronized HttpResponse doDiscard(StaplerRequest req, StaplerResponse rsp) { updating = true; - for (Iterator> it = data.entrySet().iterator(); it.hasNext();) { - Map.Entry entry = it.next(); + for (Iterator> it = data.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); if (entry.getValue().max == null) { - entry.getKey().save(); it.remove(); + tryToSave(entry.getKey()); } } updating = false; return HttpResponses.forwardToPreviousPage(); } + private void tryToSave(SaveableReference ref) { + Saveable s = ref.get(); + if (s != null) { + try { + s.save(); + } catch (Exception x) { + LOGGER.log(Level.WARNING, "failed to save " + s, x); + } + } + } + public HttpResponse doIndex(StaplerResponse rsp) throws IOException { return new HttpRedirect("manage"); } + /** Reference to a saveable object that need not actually hold it in heap. */ + private interface SaveableReference { + @CheckForNull Saveable get(); + // must also define equals, hashCode + } + + private static SaveableReference referTo(Saveable s) { + if (s instanceof Run) { + return new RunSaveableReference((Run) s); + } else { + return new SimpleSaveableReference(s); + } + } + + private static final class SimpleSaveableReference implements SaveableReference { + private final Saveable instance; + SimpleSaveableReference(Saveable instance) { + this.instance = instance; + } + @Override public Saveable get() { + return instance; + } + @Override public int hashCode() { + return instance.hashCode(); + } + @Override public boolean equals(Object obj) { + return obj instanceof SimpleSaveableReference && instance.equals(((SimpleSaveableReference) obj).instance); + } + } + + // could easily make an ItemSaveableReference, but Jenkins holds all these strongly, so why bother + + private static final class RunSaveableReference implements SaveableReference { + private final String id; + RunSaveableReference(Run r) { + id = r.getExternalizableId(); + } + @Override public Saveable get() { + try { + return Run.fromExternalizableId(id); + } catch (IllegalArgumentException x) { + // Typically meaning the job or build was since deleted. + LOGGER.log(Level.FINE, null, x); + return null; + } + } + @Override public int hashCode() { + return id.hashCode(); + } + @Override public boolean equals(Object obj) { + return obj instanceof RunSaveableReference && id.equals(((RunSaveableReference) obj).id); + } + } + @Extension public static class ManagementLinkImpl extends ManagementLink { @Override diff --git a/core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java b/core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java index 3fa520808c11f19817c71b8c72fcb07fa94a1c98..79e1685f6014b139451f17e75ed168bbbcfde750 100644 --- a/core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java +++ b/core/src/main/java/hudson/diagnosis/ReverseProxySetupMonitor.java @@ -24,15 +24,18 @@ package hudson.diagnosis; import hudson.Extension; +import hudson.Util; import hudson.model.AdministrativeMonitor; -import hudson.util.FormValidation; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.QueryParameter; -import org.kohsuke.stapler.WebMethod; import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.model.Jenkins; +import org.kohsuke.stapler.Stapler; /** * Looks out for a broken reverse proxy setup that doesn't rewrite the location header correctly. @@ -47,6 +50,9 @@ import java.io.IOException; */ @Extension public class ReverseProxySetupMonitor extends AdministrativeMonitor { + + private static final Logger LOGGER = Logger.getLogger(ReverseProxySetupMonitor.class.getName()); + @Override public boolean isActivated() { // return true to always inject an HTML fragment to perform a test @@ -54,12 +60,26 @@ public class ReverseProxySetupMonitor extends AdministrativeMonitor { } public HttpResponse doTest() { - return new HttpRedirect("test-for-reverse-proxy-setup"); + String referer = Stapler.getCurrentRequest().getReferer(); + Jenkins j = Jenkins.getInstance(); + assert j != null; + // May need to send an absolute URL, since handling of HttpRedirect with a relative URL does not currently honor X-Forwarded-Proto/Port at all. + String redirect = j.getRootUrl() + "administrativeMonitor/" + id + "/testForReverseProxySetup/" + (referer != null ? Util.rawEncode(referer) : "NO-REFERER") + "/"; + LOGGER.log(Level.FINE, "coming from {0} and redirecting to {1}", new Object[] {referer, redirect}); + return new HttpRedirect(redirect); } - @WebMethod(name="test-for-reverse-proxy-setup") - public FormValidation doFoo() { - return FormValidation.ok(); + public void getTestForReverseProxySetup(String rest) { + Jenkins j = Jenkins.getInstance(); + assert j != null; + String inferred = j.getRootUrlFromRequest() + "manage"; + // TODO this could also verify that j.getRootUrl() has been properly configured, and send a different message if not + if (rest.equals(inferred)) { + throw HttpResponses.ok(); + } else { + LOGGER.log(Level.WARNING, "{0} vs. {1}", new Object[] {inferred, rest}); + throw HttpResponses.errorWithoutStack(404, inferred + " vs. " + rest); + } } /** diff --git a/core/src/main/java/hudson/fsp/WorkspaceSnapshotSCM.java b/core/src/main/java/hudson/fsp/WorkspaceSnapshotSCM.java index 140220c54d99b4e60ebf9aec31fa90f6aa4a8b90..956f4c946412a704feb491dd7b289b45a0236b43 100644 --- a/core/src/main/java/hudson/fsp/WorkspaceSnapshotSCM.java +++ b/core/src/main/java/hudson/fsp/WorkspaceSnapshotSCM.java @@ -120,15 +120,15 @@ public class WorkspaceSnapshotSCM extends SCM { return new Snapshot(snapshot,b); } - public SCMRevisionState calcRevisionsFromBuild(AbstractBuild build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { + @Override public SCMRevisionState calcRevisionsFromBuild(AbstractBuild build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { return null; } - protected PollingResult compareRemoteRevisionWith(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException { + @Override protected PollingResult compareRemoteRevisionWith(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException { return PollingResult.NO_CHANGES; } - public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException, InterruptedException { + @Override public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException, InterruptedException { try { resolve().restoreTo(workspace,listener); return true; @@ -139,11 +139,11 @@ public class WorkspaceSnapshotSCM extends SCM { } } - public ChangeLogParser createChangeLogParser() { + @Override public ChangeLogParser createChangeLogParser() { return null; } - public SCMDescriptor getDescriptor() { + @Override public SCMDescriptor getDescriptor() { return null; } } diff --git a/core/src/main/java/hudson/init/InitializerFinder.java b/core/src/main/java/hudson/init/InitializerFinder.java index 4fdd64a299e035d25988ac7cc1e83a2e6752e1ef..8b11b5740c509271f9072e246c6aa83919dd4b03 100644 --- a/core/src/main/java/hudson/init/InitializerFinder.java +++ b/core/src/main/java/hudson/init/InitializerFinder.java @@ -23,184 +23,50 @@ */ package hudson.init; -import hudson.model.Hudson; -import jenkins.model.Jenkins; -import org.jvnet.hudson.annotation_indexer.Index; import org.jvnet.hudson.reactor.Milestone; -import org.jvnet.hudson.reactor.Task; -import org.jvnet.hudson.reactor.TaskBuilder; -import org.jvnet.hudson.reactor.MilestoneImpl; -import org.jvnet.hudson.reactor.Reactor; -import org.jvnet.localizer.ResourceBundleHolder; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.MissingResourceException; -import java.util.Set; -import java.util.logging.Logger; - -import static java.util.logging.Level.WARNING; /** * Discovers initialization tasks from {@link Initializer}. * * @author Kohsuke Kawaguchi */ -public class InitializerFinder extends TaskBuilder { - private final ClassLoader cl; - - private final Set discovered = new HashSet(); +public class InitializerFinder extends TaskMethodFinder { public InitializerFinder(ClassLoader cl) { - this.cl = cl; + super(Initializer.class,InitMilestone.class,cl); } public InitializerFinder() { this(Thread.currentThread().getContextClassLoader()); } - public Collection discoverTasks(Reactor session) throws IOException { - List result = new ArrayList(); - for (Method e : Index.list(Initializer.class,cl,Method.class)) { - if (filter(e)) continue; // already reported once - - if (!Modifier.isStatic(e.getModifiers())) - throw new IOException(e+" is not a static method"); - - Initializer i = e.getAnnotation(Initializer.class); - if (i==null) continue; // stale index - - result.add(new TaskImpl(i, e)); - } - return result; + @Override + protected String displayNameOf(Initializer i) { + return i.displayName(); } - /** - * Return true to ignore this method. - */ - protected boolean filter(Method e) { - return !discovered.add(e); + @Override + protected String[] requiresOf(Initializer i) { + return i.requires(); } - /** - * Obtains the display name of the given initialization task - */ - protected String getDisplayNameOf(Method e, Initializer i) { - Class c = e.getDeclaringClass(); - String key = i.displayName(); - if (key.length()==0) return c.getSimpleName()+"."+e.getName(); - try { - ResourceBundleHolder rb = ResourceBundleHolder.get( - c.getClassLoader().loadClass(c.getPackage().getName() + ".Messages")); - return rb.format(key); - } catch (ClassNotFoundException x) { - LOGGER.log(WARNING, "Failed to load "+x.getMessage()+" for "+e.toString(),x); - return key; - } catch (MissingResourceException x) { - LOGGER.log(WARNING, "Could not find key '" + key + "' in " + c.getPackage().getName() + ".Messages", x); - return key; - } + @Override + protected String[] attainsOf(Initializer i) { + return i.attains(); } - /** - * Invokes the given initialization method. - */ - protected void invoke(Method e) { - try { - Class[] pt = e.getParameterTypes(); - Object[] args = new Object[pt.length]; - for (int i=0; i type) { - if (type==Jenkins.class || type==Hudson.class) - return Jenkins.getInstance(); - throw new IllegalArgumentException("Unable to inject "+type); + @Override + protected Milestone beforeOf(Initializer i) { + return i.before(); } - /** - * Task implementation. - */ - public class TaskImpl implements Task { - final Collection requires; - final Collection attains; - private final Initializer i; - private final Method e; - - private TaskImpl(Initializer i, Method e) { - this.i = i; - this.e = e; - requires = toMilestones(i.requires(), i.after()); - attains = toMilestones(i.attains(), i.before()); - } - - /** - * {@link Initializer} annotaion on the {@linkplain #getMethod() method} - */ - public Initializer getAnnotation() { - return i; - } - - /** - * Static method that runs the initialization, that this task wraps. - */ - public Method getMethod() { - return e; - } - - public Collection requires() { - return requires; - } - - public Collection attains() { - return attains; - } - - public String getDisplayName() { - return getDisplayNameOf(e, i); - } - - public boolean failureIsFatal() { - return i.fatal(); - } - - public void run(Reactor session) { - invoke(e); - } - - public String toString() { - return e.toString(); - } - - private Collection toMilestones(String[] tokens, InitMilestone m) { - List r = new ArrayList(); - for (String s : tokens) { - try { - r.add(InitMilestone.valueOf(s)); - } catch (IllegalArgumentException x) { - r.add(new MilestoneImpl(s)); - } - } - r.add(m); - return r; - } + @Override + protected boolean fatalOf(Initializer i) { + return i.fatal(); } - - private static final Logger LOGGER = Logger.getLogger(InitializerFinder.class.getName()); } diff --git a/core/src/main/java/hudson/init/TaskMethodFinder.java b/core/src/main/java/hudson/init/TaskMethodFinder.java new file mode 100644 index 0000000000000000000000000000000000000000..7747e850b6f0cbe54722b726f0528875c70adf88 --- /dev/null +++ b/core/src/main/java/hudson/init/TaskMethodFinder.java @@ -0,0 +1,189 @@ +package hudson.init; + +import hudson.model.Hudson; +import jenkins.model.Jenkins; +import org.jvnet.hudson.annotation_indexer.Index; +import org.jvnet.hudson.reactor.Milestone; +import org.jvnet.hudson.reactor.MilestoneImpl; +import org.jvnet.hudson.reactor.Reactor; +import org.jvnet.hudson.reactor.Task; +import org.jvnet.hudson.reactor.TaskBuilder; +import org.jvnet.localizer.ResourceBundleHolder; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.MissingResourceException; +import java.util.Set; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; + +/** + * @author Kohsuke Kawaguchi + */ +abstract class TaskMethodFinder extends TaskBuilder { + private static final Logger LOGGER = Logger.getLogger(TaskMethodFinder.class.getName()); + protected final ClassLoader cl; + private final Set discovered = new HashSet(); + + private final Class type; + private final Class milestoneType; + + public TaskMethodFinder(Class type, Class milestoneType, ClassLoader cl) { + this.type = type; + this.milestoneType = milestoneType; + this.cl = cl; + } + + // working around the restriction that Java doesn't allow annotation types to extend interfaces + protected abstract String displayNameOf(T i); + protected abstract String[] requiresOf(T i); + protected abstract String[] attainsOf(T i); + protected abstract Milestone afterOf(T i); + protected abstract Milestone beforeOf(T i); + protected abstract boolean fatalOf(T i); + + public Collection discoverTasks(Reactor session) throws IOException { + List result = new ArrayList(); + for (Method e : Index.list(type, cl, Method.class)) { + if (filter(e)) continue; // already reported once + + if (!Modifier.isStatic(e.getModifiers())) + throw new IOException(e+" is not a static method"); + + T i = e.getAnnotation(type); + if (i==null) continue; // stale index + + result.add(new TaskImpl(i, e)); + } + return result; + } + + /** + * Return true to ignore this method. + */ + protected boolean filter(Method e) { + return !discovered.add(e); + } + + /** + * Obtains the display name of the given initialization task + */ + protected String getDisplayNameOf(Method e, T i) { + Class c = e.getDeclaringClass(); + String key = displayNameOf(i); + if (key.length()==0) return c.getSimpleName()+"."+e.getName(); + try { + ResourceBundleHolder rb = ResourceBundleHolder.get( + c.getClassLoader().loadClass(c.getPackage().getName() + ".Messages")); + return rb.format(key); + } catch (ClassNotFoundException x) { + LOGGER.log(WARNING, "Failed to load "+x.getMessage()+" for "+e.toString(),x); + return key; + } catch (MissingResourceException x) { + LOGGER.log(WARNING, "Could not find key '" + key + "' in " + c.getPackage().getName() + ".Messages", x); + return key; + } + } + + /** + * Invokes the given initialization method. + */ + protected void invoke(Method e) { + try { + Class[] pt = e.getParameterTypes(); + Object[] args = new Object[pt.length]; + for (int i=0; i type) { + if (type==Jenkins.class || type==Hudson.class) + return Jenkins.getInstance(); + throw new IllegalArgumentException("Unable to inject "+type); + } + + /** + * Task implementation. + */ + public class TaskImpl implements Task { + final Collection requires; + final Collection attains; + private final T i; + private final Method e; + + private TaskImpl(T i, Method e) { + this.i = i; + this.e = e; + requires = toMilestones(requiresOf(i), afterOf(i)); + attains = toMilestones(attainsOf(i), beforeOf(i)); + } + + /** + * The annotation on the {@linkplain #getMethod() method} + */ + public T getAnnotation() { + return i; + } + + /** + * Static method that runs the initialization, that this task wraps. + */ + public Method getMethod() { + return e; + } + + public Collection requires() { + return requires; + } + + public Collection attains() { + return attains; + } + + public String getDisplayName() { + return getDisplayNameOf(e, i); + } + + public boolean failureIsFatal() { + return fatalOf(i); + } + + public void run(Reactor session) { + invoke(e); + } + + public String toString() { + return e.toString(); + } + + private Collection toMilestones(String[] tokens, Milestone m) { + List r = new ArrayList(); + for (String s : tokens) { + try { + r.add((Milestone)Enum.valueOf(milestoneType,s)); + } catch (IllegalArgumentException x) { + r.add(new MilestoneImpl(s)); + } + } + r.add(m); + return r; + } + } +} diff --git a/core/src/main/java/hudson/init/TermMilestone.java b/core/src/main/java/hudson/init/TermMilestone.java new file mode 100644 index 0000000000000000000000000000000000000000..85efd8d5eab71aab90f674533a9c21857b32c3f7 --- /dev/null +++ b/core/src/main/java/hudson/init/TermMilestone.java @@ -0,0 +1,57 @@ +package hudson.init; + +import org.jvnet.hudson.reactor.Executable; +import org.jvnet.hudson.reactor.Milestone; +import org.jvnet.hudson.reactor.TaskBuilder; +import org.jvnet.hudson.reactor.TaskGraphBuilder; + +/** + * Various key milestone in the termination process of Jenkins. + * + *

+ * Plugins can use these milestones to execute their tear down processing at the right moment + * (in addition to defining their own milestones by implementing {@link Milestone}. + * + *

+ * These milestones are achieve in this order. + * + * @author Kohsuke Kawaguchi + */ +public enum TermMilestone implements Milestone { + /** + * The very first milestone that gets achieved without doing anything. + * + * This is used in {@link Initializer#after()} since annotations cannot have null as the default value. + */ + STARTED("Started termination"), + + /** + * The very last milestone + * + * This is used in {@link Initializer#before()} since annotations cannot have null as the default value. + */ + COMPLETED("Completed termination"); + + private final String message; + + TermMilestone(String message) { + this.message = message; + } + + /** + * Creates a set of dummy tasks to enforce ordering among {@link TermMilestone}s. + */ + public static TaskBuilder ordering() { + TaskGraphBuilder b = new TaskGraphBuilder(); + TermMilestone[] v = values(); + for (int i=0; i + * This has the identical purpose as {@link #requires()}, but it's separated to allow better type-safety + * when using {@link TermMilestone} as a requirement (since enum member definitions need to be constant.) + */ + TermMilestone after() default STARTED; + + /** + * Indicates that this terminator is a necessary step before achieving the specified milestone. + * + *

+ * This has the identical purpose as {@link #attains()}. See {@link #after()} for why there are two things + * to achieve the same goal. + */ + TermMilestone before() default COMPLETED; + + /** + * Indicates the milestones necessary before executing this terminator. + */ + String[] requires() default {}; + + /** + * Indicates the milestones that this terminator contributes to. + * + * A milestone is considered attained if all the terminators that attains the given milestone + * completes. So it works as a kind of join. + */ + String[] attains() default {}; + + /** + * Key in Messages.properties that represents what this task is about. Used for rendering the progress. + * Defaults to "${short class name}.${method Name}". + */ + String displayName() default ""; +} diff --git a/core/src/main/java/hudson/init/TerminatorFinder.java b/core/src/main/java/hudson/init/TerminatorFinder.java new file mode 100644 index 0000000000000000000000000000000000000000..6b54bf3604aaea2994c9987f23dd8a64aed633bd --- /dev/null +++ b/core/src/main/java/hudson/init/TerminatorFinder.java @@ -0,0 +1,45 @@ +package hudson.init; + +import org.jvnet.hudson.reactor.Milestone; + +/** + * @author Kohsuke Kawaguchi + */ +public class TerminatorFinder extends TaskMethodFinder { + public TerminatorFinder(ClassLoader cl) { + super(Terminator.class, TermMilestone.class, cl); + } + + @Override + protected String displayNameOf(Terminator i) { + return i.displayName(); + } + + @Override + protected String[] requiresOf(Terminator i) { + return i.requires(); + } + + @Override + protected String[] attainsOf(Terminator i) { + return i.attains(); + } + + @Override + protected Milestone afterOf(Terminator i) { + return i.after(); + } + + @Override + protected Milestone beforeOf(Terminator i) { + return i.before(); + } + + /** + * Termination code is never fatal. + */ + @Override + protected boolean fatalOf(Terminator i) { + return false; + } +} diff --git a/core/src/main/java/hudson/init/impl/GroovyInitScript.java b/core/src/main/java/hudson/init/impl/GroovyInitScript.java index cf5da2340738c881a6a919e7e1b4ec3fb2aeeefe..621d52ce635f59c8d09bc4f198a3a24e4b90b88f 100644 --- a/core/src/main/java/hudson/init/impl/GroovyInitScript.java +++ b/core/src/main/java/hudson/init/impl/GroovyInitScript.java @@ -23,20 +23,13 @@ */ package hudson.init.impl; -import groovy.lang.GroovyCodeSource; -import groovy.lang.GroovyShell; +import hudson.init.Initializer; +import jenkins.model.Jenkins; +import jenkins.util.groovy.GroovyHookScript; -import java.io.File; -import java.io.FileFilter; import java.io.IOException; -import java.net.URL; -import java.util.Arrays; -import java.util.logging.Logger; -import jenkins.model.Jenkins; -import static hudson.init.InitMilestone.JOB_LOADED; -import hudson.init.Initializer; -import java.util.logging.Level; +import static hudson.init.InitMilestone.*; /** * Run the initialization script, if it exists. @@ -45,47 +38,7 @@ import java.util.logging.Level; */ public class GroovyInitScript { @Initializer(after=JOB_LOADED) - public static void init(Jenkins j) throws IOException { - URL bundledInitScript = j.servletContext.getResource("/WEB-INF/init.groovy"); - if (bundledInitScript!=null) { - LOGGER.info("Executing bundled init script: "+bundledInitScript); - execute(new GroovyCodeSource(bundledInitScript)); - } - - File initScript = new File(j.getRootDir(),"init.groovy"); - if(initScript.exists()) { - execute(initScript); - } - - File initScriptD = new File(j.getRootDir(),"init.groovy.d"); - if (initScriptD.isDirectory()) { - File[] scripts = initScriptD.listFiles(new FileFilter() { - public boolean accept(File f) { - return f.getName().endsWith(".groovy"); - } - }); - if (scripts!=null) { - // sort to run them in a deterministic order - Arrays.sort(scripts); - for (File f : scripts) - execute(f); - } - } + public static void init(Jenkins j) { + new GroovyHookScript("init").run(); } - - private static void execute(File initScript) throws IOException { - LOGGER.info("Executing "+initScript); - execute(new GroovyCodeSource(initScript)); - } - - private static void execute(GroovyCodeSource initScript) { - GroovyShell shell = new GroovyShell(Jenkins.getInstance().getPluginManager().uberClassLoader); - try { - shell.evaluate(initScript); - } catch (RuntimeException x) { - LOGGER.log(Level.WARNING, "Failed to run script " + initScript.getName(), x); - } - } - - private static final Logger LOGGER = Logger.getLogger(GroovyInitScript.class.getName()); } diff --git a/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java b/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java index 60f34e3a7d8e93ce5391f7ff2ebcd8aaada8c7bc..e39edb8f7cd1232800fc65ff2e9f61996de65fc8 100644 --- a/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java +++ b/core/src/main/java/hudson/init/impl/InstallUncaughtExceptionHandler.java @@ -11,6 +11,7 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import org.kohsuke.stapler.Stapler; /** * @author Kohsuke Kawaguchi @@ -22,8 +23,18 @@ public class InstallUncaughtExceptionHandler { @Override public void reportException(Throwable e, ServletContext context, HttpServletRequest req, HttpServletResponse rsp) throws ServletException, IOException { req.setAttribute("javax.servlet.error.exception",e); - WebApp.get(j.servletContext).getSomeStapler() - .invoke(req,rsp, Jenkins.getInstance(), "/oops"); + try { + WebApp.get(j.servletContext).getSomeStapler() + .invoke(req,rsp, Jenkins.getInstance(), "/oops"); + } catch (ServletException x) { + if (!Stapler.isSocketException(x)) { + throw x; + } + } catch (IOException x) { + if (!Stapler.isSocketException(x)) { + throw x; + } + } } }); } diff --git a/core/src/main/java/hudson/lifecycle/Lifecycle.java b/core/src/main/java/hudson/lifecycle/Lifecycle.java index 7f7c8e9bc86bdbd2687d50b6b721c8acee9aac29..d314742f85fe9ad9d3ac23e16eb8064e4f526e04 100644 --- a/core/src/main/java/hudson/lifecycle/Lifecycle.java +++ b/core/src/main/java/hudson/lifecycle/Lifecycle.java @@ -158,7 +158,14 @@ public abstract class Lifecycle implements ExtensionPoint { public boolean canRewriteHudsonWar() { // if we don't know where jenkins.war is, it's impossible to replace. File f = getHudsonWar(); - return f!=null && f.canWrite(); + if (f == null || !f.canWrite()) { + return false; + } + File parent = f.getParentFile(); + if (parent == null || !parent.canWrite()) { + return false; + } + return true; } /** diff --git a/core/src/main/java/hudson/lifecycle/UnixLifecycle.java b/core/src/main/java/hudson/lifecycle/UnixLifecycle.java index 6bcec476fcea81fc28c8fa889aebe4f46fb1901c..6a558594532ca65ad95372afecb3cd692c3ea835 100644 --- a/core/src/main/java/hudson/lifecycle/UnixLifecycle.java +++ b/core/src/main/java/hudson/lifecycle/UnixLifecycle.java @@ -79,8 +79,8 @@ public class UnixLifecycle extends Lifecycle { } // exec to self - String exe = Daemon.getCurrentExecutable(); - LIBC.execv(exe, new StringArray(args.toArray(new String[args.size()]))); + String exe = args.get(0); + LIBC.execvp(exe, new StringArray(args.toArray(new String[args.size()]))); throw new IOException("Failed to exec '"+exe+"' "+LIBC.strerror(Native.getLastError())); } diff --git a/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java b/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java index 6a8b4bec8d44d9624c1ee30fc6995bf12c5e7f12..11f364e953ba25fce29da0a4499aa567731cb219 100644 --- a/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java +++ b/core/src/main/java/hudson/lifecycle/WindowsServiceLifecycle.java @@ -111,8 +111,11 @@ public class WindowsServiceLifecycle extends Lifecycle { File copyFiles = new File(rootDir,baseName+".copies"); FileWriter w = new FileWriter(copyFiles, true); - w.write(by.getAbsolutePath()+'>'+getHudsonWar().getAbsolutePath()+'\n'); - w.close(); + try { + w.write(by.getAbsolutePath()+'>'+getHudsonWar().getAbsolutePath()+'\n'); + } finally { + w.close(); + } } @Override @@ -123,10 +126,14 @@ public class WindowsServiceLifecycle extends Lifecycle { ByteArrayOutputStream baos = new ByteArrayOutputStream(); StreamTaskListener task = new StreamTaskListener(baos); task.getLogger().println("Restarting a service"); - File executable = new File(home, "hudson.exe"); + String exe = System.getenv("WINSW_EXECUTABLE"); + File executable; + if (exe!=null) executable = new File(exe); + else executable = new File(home, "hudson.exe"); if (!executable.exists()) executable = new File(home, "jenkins.exe"); - int r = new LocalLauncher(task).launch().cmds(executable, "restart") + // use restart! to run hudson/jenkins.exe restart in a separate process, so it doesn't kill itself + int r = new LocalLauncher(task).launch().cmds(executable, "restart!") .stdout(task).pwd(home).join(); if(r!=0) throw new IOException(baos.toString()); diff --git a/core/src/main/java/hudson/logging/LogRecorder.java b/core/src/main/java/hudson/logging/LogRecorder.java index f780252af4d4f1c9b6f4941332a17aa1414a4a6f..d3660efaeaf44d3e0a9f0210fad73267cf2e1b49 100644 --- a/core/src/main/java/hudson/logging/LogRecorder.java +++ b/core/src/main/java/hudson/logging/LogRecorder.java @@ -31,6 +31,7 @@ import hudson.Util; import hudson.XmlFile; import hudson.model.AbstractModelObject; import hudson.model.Computer; +import hudson.util.HttpResponses; import jenkins.model.Jenkins; import hudson.model.Saveable; import hudson.model.TaskListener; @@ -44,6 +45,7 @@ import hudson.util.RingBufferLogHandler; import hudson.util.XStream2; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.interceptor.RequirePOST; @@ -84,14 +86,38 @@ public class LogRecorder extends AbstractModelObject implements Saveable { public final CopyOnWriteList targets = new CopyOnWriteList(); - private transient /*almost final*/ RingBufferLogHandler handler = new RingBufferLogHandler() { + @Restricted(NoExternalUse.class) + Target[] orderedTargets() { + // will contain targets ordered by reverse name length (place specific targets at the beginning) + Target[] ts = targets.toArray(new Target[]{}); + + Arrays.sort(ts, new Comparator() { + public int compare(Target left, Target right) { + return right.getName().length() - left.getName().length(); + } + }); + + return ts; + } + + @Restricted(NoExternalUse.class) + transient /*almost final*/ RingBufferLogHandler handler = new RingBufferLogHandler() { @Override public void publish(LogRecord record) { - for (Target t : targets) { - if(t.includes(record)) { + for (Target t : orderedTargets()) { + Boolean match = t.matches(record); + if (match == null) { + // domain does not match, so continue looking + continue; + } + + if (match.booleanValue()) { + // most specific logger matches, so publish super.publish(record); - return; } + // most specific logger does not match, so don't publish + // allows reducing log level for more specific loggers + return; } } }; @@ -123,6 +149,11 @@ public class LogRecorder extends AbstractModelObject implements Saveable { return Level.parse(String.valueOf(level)); } + public String getName() { + return name; + } + + @Deprecated public boolean includes(LogRecord r) { if(r.getLevel().intValue() < level) return false; // below the threshold @@ -136,6 +167,21 @@ public class LogRecorder extends AbstractModelObject implements Saveable { return rest.startsWith(".") || rest.length()==0; } + public Boolean matches(LogRecord r) { + boolean levelSufficient = r.getLevel().intValue() >= level; + if (name.length() == 0) { + return Boolean.valueOf(levelSufficient); // include if level matches + } + String logName = r.getLoggerName(); + if(logName==null || !logName.startsWith(name)) + return null; // not in the domain of this logger + String rest = logName.substring(name.length()); + if (rest.startsWith(".") || rest.length()==0) { + return Boolean.valueOf(levelSufficient); // include if level matches + } + return null; + } + public Logger getLogger() { if (logger == null) { logger = Logger.getLogger(name); @@ -253,6 +299,12 @@ public class LogRecorder extends AbstractModelObject implements Saveable { rsp.sendRedirect2(redirect); } + @RequirePOST + public HttpResponse doClear() throws IOException { + handler.clear(); + return HttpResponses.redirectToDot(); + } + /** * Loads the settings from a file. */ @@ -299,7 +351,7 @@ public class LogRecorder extends AbstractModelObject implements Saveable { * The file we save our configuration. */ private XmlFile getConfigFile() { - return new XmlFile(XSTREAM, new File(Jenkins.getInstance().getRootDir(),"log/"+name+".xml")); + return new XmlFile(XSTREAM, new File(LogRecorderManager.configDir(), name + ".xml")); } /** diff --git a/core/src/main/java/hudson/logging/LogRecorderManager.java b/core/src/main/java/hudson/logging/LogRecorderManager.java index ab8337c701304539e5b92d5b4b7b00cbf156ebb2..d16ece6c2babdcfdf0fd9f95ddf25ce1098c0b06 100644 --- a/core/src/main/java/hudson/logging/LogRecorderManager.java +++ b/core/src/main/java/hudson/logging/LogRecorderManager.java @@ -33,7 +33,6 @@ import hudson.model.RSS; import hudson.util.CopyOnWriteMap; import jenkins.model.JenkinsLocationConfiguration; import jenkins.model.ModelObjectWithChildren; -import jenkins.model.ModelObjectWithContextMenu; import jenkins.model.ModelObjectWithContextMenu.ContextMenu; import org.apache.commons.io.filefilter.WildcardFileFilter; import org.kohsuke.stapler.QueryParameter; @@ -52,7 +51,6 @@ import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; @@ -84,12 +82,16 @@ public class LogRecorderManager extends AbstractModelObject implements ModelObje return logRecorders.get(token); } + static File configDir() { + return new File(Jenkins.getInstance().getRootDir(), "log"); + } + /** * Loads the configuration from disk. */ public void load() throws IOException { logRecorders.clear(); - File dir = new File(Jenkins.getInstance().getRootDir(), "log"); + File dir = configDir(); File[] files = dir.listFiles((FileFilter)new WildcardFileFilter("*.xml")); if(files==null) return; for (File child : files) { diff --git a/core/src/main/java/hudson/markup/EbayPolicy.java b/core/src/main/java/hudson/markup/EbayPolicy.java deleted file mode 100644 index 75b43f7a795e7944d678d7a16960646fcb25d7dd..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/markup/EbayPolicy.java +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) 2011, Mike Samuel -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions -// are met: -// -// Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// Neither the name of the OWASP nor the names of its contributors may -// be used to endorse or promote products derived from this software -// without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -// POSSIBILITY OF SUCH DAMAGE. -package hudson.markup; - -import com.google.common.base.Charsets; -import com.google.common.base.Function; -import com.google.common.base.Predicate; -import com.google.common.base.Throwables; -import com.google.common.io.CharStreams; -import org.owasp.html.Handler; -import org.owasp.html.HtmlPolicyBuilder; -import org.owasp.html.HtmlSanitizer; -import org.owasp.html.HtmlSanitizer.Policy; -import org.owasp.html.HtmlStreamEventReceiver; -import org.owasp.html.HtmlStreamRenderer; -import org.owasp.html.PolicyFactory; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.regex.Pattern; - -/** - * Based on the - * AntiSamy EBay example. - *

- * eBay (http://www.ebay.com/) is the most popular online auction site in the - * universe, as far as I can tell. It is a public site so anyone is allowed to - * post listings with rich HTML content. It's not surprising that given the - * attractiveness of eBay as a target that it has been subject to a few complex - * XSS attacks. Listings are allowed to contain much more rich content than, - * say, Slashdot- so it's attack surface is considerably larger. The following - * tags appear to be accepted by eBay (they don't publish rules): - * {@code },... - *
- */ -public class EbayPolicy { - - // Some common regular expression definitions. - - // The 16 colors defined by the HTML Spec (also used by the CSS Spec) - private static final Pattern COLOR_NAME = Pattern.compile( - "(?:aqua|black|blue|fuchsia|gray|grey|green|lime|maroon|navy|olive|purple" - + "|red|silver|teal|white|yellow)"); - - // HTML/CSS Spec allows 3 or 6 digit hex to specify color - private static final Pattern COLOR_CODE = Pattern.compile( - "(?:#(?:[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?))"); - - private static final Pattern NUMBER_OR_PERCENT = Pattern.compile( - "[0-9]+%?"); - private static final Pattern PARAGRAPH = Pattern.compile( - "(?:[\\p{L}\\p{N},'\\.\\s\\-_\\(\\)]|&[0-9]{2};)*"); - private static final Pattern HTML_ID = Pattern.compile( - "[a-zA-Z0-9\\:\\-_\\.]+"); - // force non-empty with a '+' at the end instead of '*' - private static final Pattern HTML_TITLE = Pattern.compile( - "[\\p{L}\\p{N}\\s\\-_',:\\[\\]!\\./\\\\\\(\\)&]*"); - private static final Pattern HTML_CLASS = Pattern.compile( - "[a-zA-Z0-9\\s,\\-_]+"); - - private static final Pattern ONSITE_URL = Pattern.compile( - "(?:[\\p{L}\\p{N}\\\\\\.\\#@\\$%\\+&;\\-_~,\\?=/!]+|\\#(\\w)+)"); - private static final Pattern OFFSITE_URL = Pattern.compile( - "\\s*(?:(?:ht|f)tps?://|mailto:)[\\p{L}\\p{N}]" - + "[\\p{L}\\p{N}\\p{Zs}\\.\\#@\\$%\\+&;:\\-_~,\\?=/!\\(\\)]*\\s*"); - - private static final Pattern NUMBER = Pattern.compile( - "[+-]?(?:(?:[0-9]+(?:\\.[0-9]*)?)|\\.[0-9]+)"); - - private static final Pattern NAME = Pattern.compile("[a-zA-Z0-9\\-_\\$]+"); - - private static final Pattern ALIGN = Pattern.compile( - "(?i)center|left|right|justify|char"); - - private static final Pattern VALIGN = Pattern.compile( - "(?i)baseline|bottom|middle|top"); - - private static final Predicate COLOR_NAME_OR_COLOR_CODE - = new Predicate() { - public boolean apply(String s) { - return COLOR_NAME.matcher(s).matches() - || COLOR_CODE.matcher(s).matches(); - } - }; - - private static final Predicate ONSITE_OR_OFFSITE_URL - = new Predicate() { - public boolean apply(String s) { - return ONSITE_URL.matcher(s).matches() - || OFFSITE_URL.matcher(s).matches(); - } - }; - - private static final Pattern HISTORY_BACK = Pattern.compile( - "(?:javascript:)?\\Qhistory.go(-1)\\E"); - - private static final Pattern ONE_CHAR = Pattern.compile( - ".?", Pattern.DOTALL); - - - public static final PolicyFactory POLICY_DEFINITION; - - static { - POLICY_DEFINITION = new HtmlPolicyBuilder() - .allowAttributes("id").matching(HTML_ID).globally() - .allowAttributes("class").matching(HTML_CLASS).globally() - .allowAttributes("lang").matching(Pattern.compile("[a-zA-Z]{2,20}")) - .globally() - .allowAttributes("title").matching(HTML_TITLE).globally() - .allowStyling() - .allowAttributes("align").matching(ALIGN).onElements("p") - .allowAttributes("for").matching(HTML_ID).onElements("label") - .allowAttributes("color").matching(COLOR_NAME_OR_COLOR_CODE) - .onElements("font") - .allowAttributes("face") - .matching(Pattern.compile("[\\w;, \\-]+")) - .onElements("font") - .allowAttributes("size").matching(NUMBER).onElements("font") - .allowAttributes("href").matching(ONSITE_OR_OFFSITE_URL) - .onElements("a") - .allowStandardUrlProtocols() - .allowAttributes("nohref").onElements("a") - .allowAttributes("name").matching(NAME).onElements("a") - .allowAttributes( - "onfocus", "onblur", "onclick", "onmousedown", "onmouseup") - .matching(HISTORY_BACK).onElements("a") - .requireRelNofollowOnLinks() - .allowAttributes("src").matching(ONSITE_OR_OFFSITE_URL) - .onElements("img") - .allowAttributes("name").matching(NAME) - .onElements("img") - .allowAttributes("alt").matching(PARAGRAPH) - .onElements("img") - .allowAttributes("border", "hspace", "vspace").matching(NUMBER) - .onElements("img") - .allowAttributes("border", "cellpadding", "cellspacing") - .matching(NUMBER).onElements("table") - .allowAttributes("bgcolor").matching(COLOR_NAME_OR_COLOR_CODE) - .onElements("table") - .allowAttributes("background").matching(ONSITE_URL) - .onElements("table") - .allowAttributes("align").matching(ALIGN) - .onElements("table") - .allowAttributes("noresize").matching(Pattern.compile("(?i)noresize")) - .onElements("table") - .allowAttributes("background").matching(ONSITE_URL) - .onElements("td", "th", "tr") - .allowAttributes("bgcolor").matching(COLOR_NAME_OR_COLOR_CODE) - .onElements("td", "th") - .allowAttributes("abbr").matching(PARAGRAPH) - .onElements("td", "th") - .allowAttributes("axis", "headers").matching(NAME) - .onElements("td", "th") - .allowAttributes("scope") - .matching(Pattern.compile("(?i)(?:row|col)(?:group)?")) - .onElements("td", "th") - .allowAttributes("nowrap") - .onElements("td", "th") - .allowAttributes("height", "width").matching(NUMBER_OR_PERCENT) - .onElements("table", "td", "th", "tr", "img") - .allowAttributes("align").matching(ALIGN) - .onElements("thead", "tbody", "tfoot", "img", - "td", "th", "tr", "colgroup", "col") - .allowAttributes("valign").matching(VALIGN) - .onElements("thead", "tbody", "tfoot", - "td", "th", "tr", "colgroup", "col") - .allowAttributes("charoff").matching(NUMBER_OR_PERCENT) - .onElements("td", "th", "tr", "colgroup", "col", - "thead", "tbody", "tfoot") - .allowAttributes("char").matching(ONE_CHAR) - .onElements("td", "th", "tr", "colgroup", "col", - "thead", "tbody", "tfoot") - .allowAttributes("colspan", "rowspan").matching(NUMBER) - .onElements("td", "th") - .allowAttributes("span", "width").matching(NUMBER_OR_PERCENT) - .onElements("colgroup", "col") - .allowElements( - "label", "noscript", "h1", "h2", "h3", "h4", "h5", "h6", - "p", "i", "b", "u", "strong", "em", "small", "big", "pre", "code", - "cite", "samp", "sub", "sup", "strike", "center", "blockquote", - "hr", "br", "col", "font", "map", "span", "div", "img", - "ul", "ol", "li", "dd", "dt", "dl", "tbody", "thead", "tfoot", - "table", "td", "th", "tr", "colgroup", "fieldset", "legend") - .toFactory(); - } - - public static void main(String[] args) throws IOException { - if (args.length != 0) { - System.err.println("Reads from STDIN and writes to STDOUT"); - System.exit(-1); - } - System.err.println("[Reading from STDIN]"); - // Fetch the HTML to sanitize. - String html = CharStreams.toString( - new InputStreamReader(System.in, Charsets.UTF_8)); - // Set up an output channel to receive the sanitized HTML. - HtmlStreamRenderer renderer = HtmlStreamRenderer.create( - System.out, - // Receives notifications on a failure to write to the output. - new Handler() { - public void handle(IOException ex) { - Throwables.propagate(ex); // System.out suppresses IOExceptions - } - }, - // Our HTML parser is very lenient, but this receives notifications on - // truly bizarre inputs. - new Handler() { - public void handle(String x) { - throw new AssertionError(x); - } - } - ); - // Use the policy defined above to sanitize the HTML. - HtmlSanitizer.sanitize(html, POLICY_DEFINITION.apply(renderer)); - } -} \ No newline at end of file diff --git a/core/src/main/java/hudson/markup/EscapedMarkupFormatter.java b/core/src/main/java/hudson/markup/EscapedMarkupFormatter.java new file mode 100644 index 0000000000000000000000000000000000000000..f106b5de2c6f00520a0656420c780143f0cb0f33 --- /dev/null +++ b/core/src/main/java/hudson/markup/EscapedMarkupFormatter.java @@ -0,0 +1,60 @@ +/* + * The MIT License + * + * Copyright 2011 Seiji Sogabe + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.markup; + +import hudson.Extension; +import hudson.Util; +import hudson.markup.MarkupFormatter; +import hudson.markup.MarkupFormatterDescriptor; +import java.io.IOException; +import java.io.Writer; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * @link MarkupFormatter} that treats the input as the escaped html. + * + * @author Seiji Sogabe + * @since 1.553 + */ +public class EscapedMarkupFormatter extends MarkupFormatter { + + @DataBoundConstructor + public EscapedMarkupFormatter() { + } + + @Override + public void translate(String markup, Writer output) throws IOException { + output.write(Util.escape(markup)); + } + + @Extension + public static class DescriptorImpl extends MarkupFormatterDescriptor { + + @Override + public String getDisplayName() { + return Messages.EscapedMarkupFormatter_DisplayName(); + } + } + +} diff --git a/core/src/main/java/hudson/markup/HtmlPolicyBuilder2.java b/core/src/main/java/hudson/markup/HtmlPolicyBuilder2.java deleted file mode 100644 index 4f6ea5435e2343927bef0beaa7d3004c89e10698..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/markup/HtmlPolicyBuilder2.java +++ /dev/null @@ -1,42 +0,0 @@ -package hudson.markup; - -import com.google.common.base.Predicate; -import org.owasp.html.HtmlPolicyBuilder; - -import java.util.regex.Pattern; - -/** - * {@link HtmlPolicyBuilder} with additional - * functions to simplify transcoding policy definition - * from OWASP AntiSamy policy files. - * - * @author Kohsuke Kawaguchi - */ -class HtmlPolicyBuilder2 extends HtmlPolicyBuilder { - public void tag(String names, Object... attributes) { - String[] tags = names.split(","); - for (int i=0; iOWASP AntiSamy MySpace Policy - */ -public class MyspacePolicy { - public static final PolicyFactory POLICY_DEFINITION; - - private static final Pattern ONSITE_URL = Pattern.compile( - "(?:[\\p{L}\\p{N}\\\\\\.\\#@\\$%\\+&;\\-_~,\\?=/!]+|\\#(\\w)+)"); - private static final Pattern OFFSITE_URL = Pattern.compile( - "\\s*(?:(?:ht|f)tps?://|mailto:)[\\p{L}\\p{N}]" - + "[\\p{L}\\p{N}\\p{Zs}\\.\\#@\\$%\\+&;:\\-_~,\\?=/!\\(\\)]*\\s*"); - - private static final Predicate ONSITE_OR_OFFSITE_URL - = new Predicate() { - public boolean apply(String s) { - return ONSITE_URL.matcher(s).matches() - || OFFSITE_URL.matcher(s).matches(); - } - }; - - static { - POLICY_DEFINITION = new HtmlPolicyBuilder2() {{ - allowAttributes("id","class","lang","title", - "alt","style","media","href","name","shape", - "border","cellpadding","cellspacing","colspan","rowspan", - "background","bgcolor","abbr","headers","charoff","char", - "aixs","nowrap","width","height","align","valign","scope", - "tabindex","disabled","readonly","accesskey","size", - "autocomplete","rows","cols").globally(); - - disallowElements( - // I'm allowing iframe - "script","noscript",/*"iframe",*/"frameset","frame"); - - tag("label", "for"); - tag("form", "action",ONSITE_OR_OFFSITE_URL, - "method"); - tag("button", "value", "type"); - tag("input", "maxlength","checked", - "src",ONSITE_OR_OFFSITE_URL, - "usemap",ONSITE_URL, - "type","value"); - tag("select", "multiple"); - tag("option", "value","label","selected"); - tag("textarea"); - tag("h1,h2,h3,h4,h5,h6,p,i,b,u,strong,em,small,big,pre,code,cite,samp,sub,sup,strike,center,blockquote"); - tag("hr,br,col"); - tag("font", "color", "face", "size"); - tag("a", "nohref","rel"); - tag("style", "type"); - tag("span,div"); - tag("img", "src",ONSITE_OR_OFFSITE_URL, - "hspace","vspace"); - tag("iframe", "src"); - tag("ul,ol,li,dd,dl,dt,thead,tbody,tfoot"); - tag("table", "noresize"); - tag("td,th,tr"); - tag("colgroup", "span"); - tag("col", "span"); - tag("fieldset,legend"); - allowStandardUrlProtocols(); - }}.toFactory(); - } - - public static void main(String[] args) throws IOException { - // Fetch the HTML to sanitize. - String html = "Google"; - // Set up an output channel to receive the sanitized HTML. - HtmlStreamRenderer renderer = HtmlStreamRenderer.create( - System.out, - // Receives notifications on a failure to write to the output. - new Handler() { - public void handle(IOException ex) { - Throwables.propagate(ex); // System.out suppresses IOExceptions - } - }, - // Our HTML parser is very lenient, but this receives notifications on - // truly bizarre inputs. - new Handler() { - public void handle(String x) { - throw new AssertionError(x); - } - } - ); - // Use the policy defined above to sanitize the HTML. - HtmlSanitizer.sanitize(html, POLICY_DEFINITION.apply(renderer)); - } -} diff --git a/core/src/main/java/hudson/markup/RawHtmlMarkupFormatter.java b/core/src/main/java/hudson/markup/RawHtmlMarkupFormatter.java deleted file mode 100644 index 89b0eca43dc153513ffcd89107ea48d9cd0df1f6..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/markup/RawHtmlMarkupFormatter.java +++ /dev/null @@ -1,71 +0,0 @@ -package hudson.markup; - -import com.google.common.base.Throwables; -import hudson.Extension; -import org.kohsuke.stapler.DataBoundConstructor; -import org.owasp.html.Handler; -import org.owasp.html.HtmlSanitizer; -import org.owasp.html.HtmlStreamRenderer; - -import java.io.IOException; -import java.io.Writer; - -/** - * {@link MarkupFormatter} that treats the input as the raw html. - * This is the backward compatible behaviour. - * - * @author Kohsuke Kawaguchi - */ -public class RawHtmlMarkupFormatter extends MarkupFormatter { - - final boolean disableSyntaxHighlighting; - - @DataBoundConstructor - public RawHtmlMarkupFormatter(final boolean disableSyntaxHighlighting) { - this.disableSyntaxHighlighting = disableSyntaxHighlighting; - } - - public boolean isDisableSyntaxHighlighting() { - return disableSyntaxHighlighting; - } - - @Override - public void translate(String markup, Writer output) throws IOException { - HtmlStreamRenderer renderer = HtmlStreamRenderer.create( - output, - // Receives notifications on a failure to write to the output. - new Handler() { - public void handle(IOException ex) { - Throwables.propagate(ex); // System.out suppresses IOExceptions - } - }, - // Our HTML parser is very lenient, but this receives notifications on - // truly bizarre inputs. - new Handler() { - public void handle(String x) { - throw new Error(x); - } - } - ); - // Use the policy defined above to sanitize the HTML. - HtmlSanitizer.sanitize(markup, MyspacePolicy.POLICY_DEFINITION.apply(renderer)); - } - - public String getCodeMirrorMode() { - return disableSyntaxHighlighting ? null : "htmlmixed"; - } - - public String getCodeMirrorConfig() { - return "mode:'text/html'"; - } - - @Extension - public static class DescriptorImpl extends MarkupFormatterDescriptor { - @Override - public String getDisplayName() { - return "Raw HTML"; - } - } - - public static final MarkupFormatter INSTANCE = new RawHtmlMarkupFormatter(false); -} diff --git a/core/src/main/java/hudson/matrix/Axis.java b/core/src/main/java/hudson/matrix/Axis.java deleted file mode 100644 index faec4501e1d3696f0960e5e22d9c555ffd0b48ac..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/Axis.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.DescriptorExtensionList; -import hudson.ExtensionPoint; -import hudson.RestrictedSince; -import hudson.Util; -import hudson.matrix.MatrixBuild.MatrixBuildExecution; -import hudson.model.AbstractDescribableImpl; -import jenkins.model.Jenkins; -import hudson.util.QuotedStringTokenizer; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.DataBoundConstructor; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.List; -import java.util.Arrays; -import java.util.Map; - -/** - * Configuration axis. - * - *

- * This class represents a single dimension of the configuration matrix. - * For example, the JAX-WS RI test configuration might include - * one axis "container={glassfish,tomcat,jetty}" and another axis - * "stax={sjsxp,woodstox}", and so on. - * - * @author Kohsuke Kawaguchi - */ -public class Axis extends AbstractDescribableImpl implements Comparable, Iterable, ExtensionPoint { - /** - * Name of this axis. - * Used as a variable name. - * - * @deprecated as of 1.373 - * Use {@link #getName()} - */ - public final String name; - - /** - * Possible values for this axis. - * - * @deprecated as of 1.373 - * Use {@link #getValues()} - */ - @Restricted(NoExternalUse.class) - @RestrictedSince("1.463") - public final List values; - - public Axis(String name, List values) { - if (values == null || values.isEmpty()) { - values = Collections.emptyList(); - } - this.name = name; - this.values = new ArrayList(values); - } - - public Axis(String name, String... values) { - this(name,Arrays.asList(values)); - } - - /** - * Used to build {@link Axis} from form. - * - * Axis with empty values need to be removed later. - */ - @DataBoundConstructor - public Axis(String name, String valueString) { - this.name = name; - this.values = new ArrayList(Arrays.asList(Util.tokenize(valueString))); - } - - /** - * Returns true if this axis is a system-reserved axis - * that has used to have af special treatment. - * - * @deprecated as of 1.373 - * System vs user difference are generalized into extension point. - */ - public boolean isSystem() { - return false; - } - - public Iterator iterator() { - return getValues().iterator(); - } - - public int size() { - return getValues().size(); - } - - public String value(int index) { - return getValues().get(index); - } - - /** - * The inverse of {@link #value(int)}. - */ - public int indexOf(String value) { - return values.indexOf(value); - } - - /** - * Axis is fully ordered so that we can convert between a list of axis - * and a string unambiguously. - */ - public int compareTo(Axis that) { - return this.name.compareTo(that.name); - } - - /** - * Name of this axis. - * Used as a variable name. - */ - public String getName() { - return name; - } - - /** - * Possible values for this axis. - */ - public List getValues() { - return Collections.unmodifiableList(values); - } - - /** - * Called right at the beginning of {@link MatrixBuild} execution to allow {@link Axis} to update {@link #values} - * based on the current build. - * - *

- * Historically, axes values are considered static. They were assumed to reflect what the user has typed in, - * and their values are changed only when the project is reconfigured. So abstractions are built around this - * notion, and so for example {@link MatrixProject} has the current axes and their values, which it uses - * to render its UI. - * - *

- * So when the need was identified to change the values of axes per build, we decided that this be represented - * as a kind of project configuration update (where a project gets reconfigured every time a build runs), and - * this call back was added to allow {@link Axis} to update the next return value from the {@link #getValues()} - * (which is typically done by updating {@link #values}.) - * - *

- * While it is not strictly required, because of these historical reasons, UI will look better if - * Future calls to {@link Axis#getValues()} return the same values as what this method returns (until - * the next rebuild call). - * - * @param context - * The ongoing build. Never null. - * @return - * Never null. Returns the updated set of values. - * @since 1.471 - */ - public List rebuild(MatrixBuildExecution context) { - return getValues(); - } - - @Override - public AxisDescriptor getDescriptor() { - return (AxisDescriptor)super.getDescriptor(); - } - - @Override - public String toString() { - return new StringBuilder().append(name).append("={").append(Util.join(values,",")).append('}').toString(); - } - - /** - * Used for generating the config UI. - * If the axis is big and occupies a lot of space, use newline for separator - * to display multi-line text. - */ - public String getValueString() { - int len=0; - for (String value : values) - len += value.length(); - char delim = len>30 ? '\n' : ' '; - // Build string connected with delimiter, quoting as needed - StringBuilder buf = new StringBuilder(len+values.size()*3); - for (String value : values) - buf.append(delim).append(QuotedStringTokenizer.quote(value,"")); - return buf.substring(1); - } - - /** - * Parses the submitted form (where possible values are - * presented as a list of checkboxes) and creates an axis - */ - public static Axis parsePrefixed(StaplerRequest req, String name) { - List values = new ArrayList(); - String prefix = name+'.'; - - Enumeration e = req.getParameterNames(); - while (e.hasMoreElements()) { - String paramName = (String) e.nextElement(); - if(paramName.startsWith(prefix)) - values.add(paramName.substring(prefix.length())); - } - if(values.isEmpty()) - return null; - return new Axis(name,values); - } - - /** - * Previously we used to persist {@link Axis}, but now those are divided into subtypes. - * So upon deserialization, resolve to the proper type. - */ - public Object readResolve() { - if (getClass()!=Axis.class) return this; - - /* - This method is necessary only because earlier versions of Jenkins treated - axis names "label" and "jdk" differently, - plus Axis was a concrete class, and we need to be able to read that back. - So this measure is not needed for newly added Axes. - */ - if (getName().equals("jdk")) - return new JDKAxis(getValues()); - if (getName().equals("label")) - return new LabelAxis(getName(),getValues()); - return new TextAxis(getName(),getValues()); - } - - /** - * Returns all the registered {@link AxisDescriptor}s. - */ - public static DescriptorExtensionList all() { - return Jenkins.getInstance().getDescriptorList(Axis.class); - } - - /** - * Converts the selected value (which is among {@link #values}) and adds that to the given map, - * which serves as the build variables. - */ - public void addBuildVariable(String value, Map map) { - map.put(name,value); - } -} diff --git a/core/src/main/java/hudson/matrix/AxisDescriptor.java b/core/src/main/java/hudson/matrix/AxisDescriptor.java deleted file mode 100644 index 7d93505e4e298db101b76beefb60fcd73e7d408f..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/AxisDescriptor.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2010, InfraDNA, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.Util; -import hudson.model.Descriptor; -import hudson.model.Failure; -import jenkins.model.Jenkins; -import hudson.util.FormValidation; -import org.kohsuke.stapler.QueryParameter; - -/** - * {@link Descriptor} for {@link Axis} - * - * @author Kohsuke Kawaguchi - */ -public abstract class AxisDescriptor extends Descriptor { - protected AxisDescriptor(Class clazz) { - super(clazz); - } - - protected AxisDescriptor() { - } - - /** - * Return false if the user shouldn't be able to create thie axis from the UI. - */ - public boolean isInstantiable() { - return true; - } - - /** - * Makes sure that the given name is good as a axis name. - */ - public FormValidation doCheckName(@QueryParameter String value) { - if(Util.fixEmpty(value)==null) - return FormValidation.ok(); - - try { - Jenkins.checkGoodName(value); - return FormValidation.ok(); - } catch (Failure e) { - return FormValidation.error(e.getMessage()); - } - } -} diff --git a/core/src/main/java/hudson/matrix/AxisList.java b/core/src/main/java/hudson/matrix/AxisList.java deleted file mode 100644 index a93a16746b1c93ee9293eccdfbfb847db4078533..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/AxisList.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import com.google.common.base.Function; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import com.thoughtworks.xstream.XStream; -import hudson.Util; -import hudson.util.RobustCollectionConverter; - -import javax.annotation.Nullable; -import java.util.*; - -/** - * List of {@link Axis}. - * - * @author Kohsuke Kawaguchi - */ -public class AxisList extends ArrayList { - public AxisList() { - } - - public AxisList(Collection c) { - super(c); - } - - public AxisList(Axis... c) { - this(Arrays.asList(c)); - } - - public Axis find(String name) { - for (Axis a : this) { - if(a.name.equals(name)) - return a; - } - return null; - } - - /** - * Creates a subset of the list that only contains the type assignable to the specified type. - */ - public AxisList subList(Class subType) { - return new AxisList(Util.filter(this,subType)); - } - - @Override - public boolean add(Axis axis) { - return axis!=null && super.add(axis); - } - - /** - * List up all the possible combinations of this list. - */ - public Iterable list() { - List> axesList = Lists.newArrayList(); - for (Axis axis : this) - axesList.add(new LinkedHashSet(axis.getValues())); - - return Iterables.transform(Sets.cartesianProduct(axesList), new Function, Combination>() { - public Combination apply(@Nullable List strings) { - assert strings != null; - return new Combination(AxisList.this, (String[]) strings.toArray(new String[0])); - } - }); - } - - /** - * {@link com.thoughtworks.xstream.converters.Converter} implementation for XStream. - */ - public static final class ConverterImpl extends RobustCollectionConverter { - public ConverterImpl(XStream xs) { - super(xs); - } - - @Override - public boolean canConvert(Class type) { - return type==AxisList.class; - } - - @Override - protected Object createCollection(Class type) { - return new AxisList(); - } - } -} diff --git a/core/src/main/java/hudson/matrix/Combination.java b/core/src/main/java/hudson/matrix/Combination.java deleted file mode 100644 index 76782a9df3a24853a5c6793403e79768fb2c2122..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/Combination.java +++ /dev/null @@ -1,299 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.Util; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.TreeMap; -import static java.lang.Boolean.TRUE; - -import groovy.lang.Binding; -import groovy.lang.GroovyShell; - -/** - * A particular combination of {@link Axis} values. - * - * For example, when axes are "x={1,2},y={3,4}", then - * [x=1,y=3] is a combination (out of 4 possible combinations) - * - * @author Kohsuke Kawaguchi - */ -public final class Combination extends TreeMap implements Comparable { - - public Combination(AxisList axisList, List values) { - for(int i=0; i keyValuePairs) { - for (Map.Entry e : keyValuePairs.entrySet()) - super.put(e.getKey(),e.getValue()); - } - - public String get(Axis a) { - return get(a.getName()); - } - - /** - * Obtains the continuous unique index number of this {@link Combination} - * in the given {@link AxisList}. - */ - public int toIndex(AxisList axis) { - int r = 0; - for (Axis a : axis) { - r *= a.size(); - r += a.indexOf(get(a)); - } - return r; - } - - /** - * Obtains a number N such that "N%M==0" would create - * a reasonable sparse matrix for integer M. - * - *

- * This is bit like {@link #toIndex(AxisList)}, but instead - * of creating a continuous number (which often maps different - * values of the same axis to the same index in modulo N residue ring, - * we use a prime number P as the base. I think this guarantees the uniform - * distribution in any N smaller than 2P (but proof, anyone?) - */ - private long toModuloIndex(AxisList axis) { - long r = 0; - for (Axis a : axis) { - r += a.indexOf(get(a)); - r *= 31; - } - return r; - } - - /** - * Evaluates the given Groovy expression with values bound from this combination. - * - *

- * For example, if this combination is a=X,b=Y, then expressions like a=="X" would evaluate to - * true. - */ - public boolean evalGroovyExpression(AxisList axes, String expression) { - - return evalGroovyExpression(axes, expression, new Binding()); - } - - /** - * @see #evalGroovyExpression(AxisList, String) - * @since 1.515 - */ - public boolean evalGroovyExpression(AxisList axes, String expression, Binding binding) { - if(Util.fixEmptyAndTrim(expression)==null) - return true; - - for (Map.Entry e : entrySet()) - binding.setVariable(e.getKey(),e.getValue()); - - binding.setVariable("index",toModuloIndex(axes)); - binding.setVariable("uniqueId",toIndex(axes)); - - GroovyShell shell = new GroovyShell(binding); - - Object result = shell.evaluate("use("+BooleanCategory.class.getName().replace('$','.')+") {"+expression+"}"); - return TRUE.equals(result); - } - - public int compareTo(Combination that) { - int d = this.size()-that.size(); - if(d!=0) return d; - - Iterator> itr = this.entrySet().iterator(); - Iterator> jtr = that.entrySet().iterator(); - while(itr.hasNext()) { - Map.Entry i = itr.next(); - Map.Entry j = jtr.next(); - - d = i.getKey().compareTo(j.getKey()); - if(d!=0) return d; - d = i.getValue().compareTo(j.getValue()); - if(d!=0) return d; - } - return 0; - } - - /** - * Works like {@link #toString()} but only include the given axes. - */ - public String toString(Collection subset) { - if(size()==1 && subset.size()==1) - return values().iterator().next(); - - StringBuilder buf = new StringBuilder(); - for (Axis a : subset) { - if(buf.length()>0) buf.append(','); - buf.append(a.getName()).append('=').append(get(a)); - } - if(buf.length()==0) buf.append("default"); // special case to avoid 0-length name. - return buf.toString(); - } - - /** - * Gets the values that correspond to the specified axes, in their order. - */ - public List values(Collection axes) { - List r = new ArrayList(axes.size()); - for (Axis a : axes) - r.add(get(a)); - return r; - } - - /** - * Converts to the ID string representation: - * axisName=value,axisName=value,... - * - * @param sep1 - * The separator between multiple axes. - * @param sep2 - * The separator between axis name and value. - */ - public String toString(char sep1, char sep2) { - StringBuilder buf = new StringBuilder(); - for (Map.Entry e : entrySet()) { - if(buf.length()>0) buf.append(sep1); - buf.append(e.getKey()).append(sep2).append(e.getValue()); - } - if(buf.length()==0) buf.append("default"); // special case to avoid 0-length name. - return buf.toString(); - } - - @Override - public String toString() { - return toString(',','='); - } - - /** - * Gets the 8 character-wide hash code for this combination - */ - public String digest() { - return Util.getDigestOf(toString()); - } - - /** - * Reverse operation of {@link #toString()}. - */ - public static Combination fromString(String id) { - if(id.equals("default")) - return new Combination(Collections.emptyMap()); - - Map m = new HashMap(); - StringTokenizer tokens = new StringTokenizer(id, ","); - while(tokens.hasMoreTokens()) { - String token = tokens.nextToken(); - int idx = token.indexOf('='); - if(idx<0) - throw new IllegalArgumentException("Can't parse "+id); - m.put(token.substring(0,idx),token.substring(idx+1)); - } - return new Combination(m); - } - - /** - * Creates compact string representation suitable for display purpose. - * - *

- * The string is made compact by looking for {@link Axis} whose values - * are unique, and omit the axis name. - */ - public String toCompactString(AxisList axes) { - Set nonUniqueAxes = new HashSet(); - Map axisByValue = new HashMap(); - - for (Axis a : axes) { - for (String v : a.getValues()) { - Axis old = axisByValue.put(v,a); - if(old!=null) { - // these two axes have colliding values - nonUniqueAxes.add(old.getName()); - nonUniqueAxes.add(a.getName()); - } - } - } - - StringBuilder buf = new StringBuilder(); - for (Map.Entry e : entrySet()) { - if(buf.length()>0) buf.append(','); - if(nonUniqueAxes.contains(e.getKey())) - buf.append(e.getKey()).append('='); - buf.append(e.getValue()); - } - if(buf.length()==0) buf.append("default"); // special case to avoid 0-length name. - return buf.toString(); - } - - // read-only - @Override - public void clear() { - throw new UnsupportedOperationException(); - } - - @Override - public void putAll(Map map) { - throw new UnsupportedOperationException(); - } - - @Override - public String put(String key, String value) { - throw new UnsupportedOperationException(); - } - - @Override - public String remove(Object key) { - throw new UnsupportedOperationException(); - } - - /** - * Duck-typing for boolean expressions. - * - * @see Combination#evalGroovyExpression(AxisList,String) - */ - public static final class BooleanCategory { - /** - * x -> y - */ - public static Boolean implies(Boolean lhs, Boolean rhs) { - return !lhs || rhs; - } - } -} diff --git a/core/src/main/java/hudson/matrix/DefaultAxisDescriptor.java b/core/src/main/java/hudson/matrix/DefaultAxisDescriptor.java deleted file mode 100644 index 96fb461499571fb07e49acd69713dda9d2cbd16a..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/DefaultAxisDescriptor.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2010, InfraDNA, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.Extension; - -/** - * {@link AxisDescriptor} for manually entered default axis. - * - * @author Kohsuke Kawaguchi - */ -@Extension -public class DefaultAxisDescriptor extends AxisDescriptor { - public DefaultAxisDescriptor() { - super(Axis.class); - } - - @Override - public String getDisplayName() { - return "Axis"; - } - - @Override - public boolean isInstantiable() { - return false; - } -} diff --git a/core/src/main/java/hudson/matrix/DefaultMatrixExecutionStrategyImpl.java b/core/src/main/java/hudson/matrix/DefaultMatrixExecutionStrategyImpl.java deleted file mode 100644 index 9541d907a559a06085a70ecc2a97616ab51fd10f..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/DefaultMatrixExecutionStrategyImpl.java +++ /dev/null @@ -1,335 +0,0 @@ -package hudson.matrix; - -import groovy.lang.Binding; -import groovy.lang.GroovyRuntimeException; -import hudson.AbortException; -import hudson.Extension; -import hudson.Util; -import hudson.console.ModelHyperlinkNote; -import hudson.matrix.MatrixBuild.MatrixBuildExecution; -import hudson.matrix.listeners.MatrixBuildListener; -import hudson.model.BuildListener; -import hudson.model.Cause.UpstreamCause; -import hudson.model.ParameterValue; -import hudson.model.ParametersAction; -import hudson.model.Queue; -import hudson.model.ResourceController; -import hudson.model.Result; -import hudson.model.Run; -import org.kohsuke.stapler.DataBoundConstructor; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.io.PrintStream; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.TreeSet; - -/** - * {@link MatrixExecutionStrategy} that captures historical behavior. - * - *

- * This class is somewhat complex because historically this wasn't an extension point and so - * people tried to put various logics that cover different use cases into one place. - * Going forward, people are encouraged to create subtypes to implement a custom logic that suits their needs. - * - * @author Kohsuke Kawaguchi - * @since 1.456 - */ -public class DefaultMatrixExecutionStrategyImpl extends MatrixExecutionStrategy { - private volatile boolean runSequentially; - - /** - * Filter to select a number of combinations to build first - */ - private volatile String touchStoneCombinationFilter; - - /** - * Required result on the touchstone combinations, in order to - * continue with the rest - */ - private volatile Result touchStoneResultCondition; - - private volatile MatrixConfigurationSorter sorter; - - @DataBoundConstructor - public DefaultMatrixExecutionStrategyImpl(Boolean runSequentially, boolean hasTouchStoneCombinationFilter, String touchStoneCombinationFilter, Result touchStoneResultCondition, MatrixConfigurationSorter sorter) { - this(runSequentially!=null ? runSequentially : false, - hasTouchStoneCombinationFilter ? touchStoneCombinationFilter : null, - hasTouchStoneCombinationFilter ? touchStoneResultCondition : null, - sorter); - } - - public DefaultMatrixExecutionStrategyImpl(boolean runSequentially, String touchStoneCombinationFilter, Result touchStoneResultCondition, MatrixConfigurationSorter sorter) { - this.runSequentially = runSequentially; - this.touchStoneCombinationFilter = touchStoneCombinationFilter; - this.touchStoneResultCondition = touchStoneResultCondition; - this.sorter = sorter; - } - - public DefaultMatrixExecutionStrategyImpl() { - this(false,false,null,null,null); - } - - public boolean getHasTouchStoneCombinationFilter() { - return touchStoneCombinationFilter!=null; - } - - /** - * If true, {@link MatrixRun}s are run sequentially, instead of running in parallel. - * - * TODO: this should be subsumed by {@link ResourceController}. - */ - public boolean isRunSequentially() { - return runSequentially; - } - - public void setRunSequentially(boolean runSequentially) { - this.runSequentially = runSequentially; - } - - public String getTouchStoneCombinationFilter() { - return touchStoneCombinationFilter; - } - - public void setTouchStoneCombinationFilter(String touchStoneCombinationFilter) { - this.touchStoneCombinationFilter = touchStoneCombinationFilter; - } - - public Result getTouchStoneResultCondition() { - return touchStoneResultCondition; - } - - public void setTouchStoneResultCondition(Result touchStoneResultCondition) { - this.touchStoneResultCondition = touchStoneResultCondition; - } - - public MatrixConfigurationSorter getSorter() { - return sorter; - } - - public void setSorter(MatrixConfigurationSorter sorter) { - this.sorter = sorter; - } - - @Override - public Result run(MatrixBuildExecution execution) throws InterruptedException, IOException { - - Collection touchStoneConfigurations = new HashSet(); - Collection delayedConfigurations = new HashSet(); - - filterConfigurations( - execution, - touchStoneConfigurations, - delayedConfigurations - ); - - if (notifyStartBuild(execution.getAggregators())) return Result.FAILURE; - - if (sorter != null) { - touchStoneConfigurations = createTreeSet(touchStoneConfigurations, sorter); - delayedConfigurations = createTreeSet(delayedConfigurations, sorter); - } - - if(!runSequentially) - for(MatrixConfiguration c : touchStoneConfigurations) - scheduleConfigurationBuild(execution, c); - - Result r = Result.SUCCESS; - for (MatrixConfiguration c : touchStoneConfigurations) { - if(runSequentially) - scheduleConfigurationBuild(execution, c); - MatrixRun run = waitForCompletion(execution, c); - notifyEndBuild(run,execution.getAggregators()); - r = r.combine(getResult(run)); - } - - PrintStream logger = execution.getListener().getLogger(); - - if (touchStoneResultCondition != null && r.isWorseThan(touchStoneResultCondition)) { - logger.printf("Touchstone configurations resulted in %s, so aborting...%n", r); - return r; - } - - if(!runSequentially) - for(MatrixConfiguration c : delayedConfigurations) - scheduleConfigurationBuild(execution, c); - - for (MatrixConfiguration c : delayedConfigurations) { - if(runSequentially) - scheduleConfigurationBuild(execution, c); - MatrixRun run = waitForCompletion(execution, c); - notifyEndBuild(run,execution.getAggregators()); - logger.println(Messages.MatrixBuild_Completed(ModelHyperlinkNote.encodeTo(c), getResult(run))); - r = r.combine(getResult(run)); - } - - return r; - } - - private void filterConfigurations( - final MatrixBuildExecution execution, - final Collection touchStoneConfigurations, - final Collection delayedConfigurations - ) throws AbortException { - - final MatrixBuild build = execution.getBuild(); - - final String combinationFilter = execution.getProject().getCombinationFilter(); - final String touchStoneFilter = getTouchStoneCombinationFilter(); - - try { - - for (MatrixConfiguration c: execution.getActiveConfigurations()) { - - if (!MatrixBuildListener.buildConfiguration(build, c)) continue; // skip rebuild - - final Combination combination = c.getCombination(); - - if (touchStoneFilter != null && satisfies(execution, combination, touchStoneFilter)) { - touchStoneConfigurations.add(c); - } else if (satisfies(execution, combination, combinationFilter)) { - delayedConfigurations.add(c); - } - } - } catch (GroovyRuntimeException ex) { - - PrintStream logger = execution.getListener().getLogger(); - logger.println(ex.getMessage()); - ex.printStackTrace(logger); - throw new AbortException("Failed executing combination filter"); - } - } - - private boolean satisfies( - final MatrixBuildExecution execution, - final Combination combination, - final String filter - ) { - - return combination.evalGroovyExpression( - execution.getProject().getAxes(), - filter, - getConfiguredBinding(execution) - ); - } - - private Binding getConfiguredBinding(final MatrixBuildExecution execution) { - - final Binding binding = new Binding(); - final ParametersAction parameters = execution.getBuild().getAction(ParametersAction.class); - - if (parameters == null) return binding; - - for (final ParameterValue pv: parameters) { - - if (pv == null) continue; - final String name = pv.getName(); - final String value = pv.createVariableResolver(null).resolve(name);; - binding.setVariable(name, value); - } - - return binding; - } - - private Result getResult(@Nullable MatrixRun run) { - // null indicates that the run was cancelled before it even gets going - return run!=null ? run.getResult() : Result.ABORTED; - } - - private boolean notifyStartBuild(List aggregators) throws InterruptedException, IOException { - for (MatrixAggregator a : aggregators) - if(!a.startBuild()) - return true; - return false; - } - - private void notifyEndBuild(MatrixRun b, List aggregators) throws InterruptedException, IOException { - if (b==null) return; // can happen if the configuration run gets cancelled before it gets started. - for (MatrixAggregator a : aggregators) - if(!a.endRun(b)) - throw new AbortException(); - } - - private TreeSet createTreeSet(Collection items, Comparator sorter) { - TreeSet r = new TreeSet(sorter); - r.addAll(items); - return r; - } - - /** Function to start schedule a single configuration - * - * This function schedule a build of a configuration passing all of the Matrixchild actions - * that are present in the parent build. - * - * @param exec Matrix build that is the parent of the configuration - * @param c Configuration to schedule - */ - private void scheduleConfigurationBuild(MatrixBuildExecution exec, MatrixConfiguration c) { - MatrixBuild build = exec.getBuild(); - exec.getListener().getLogger().println(Messages.MatrixBuild_Triggering(ModelHyperlinkNote.encodeTo(c))); - - // filter the parent actions for those that can be passed to the individual jobs. - List childActions = Util.filter(build.getActions(), MatrixChildAction.class); - c.scheduleBuild(childActions, new UpstreamCause((Run)build)); - } - - private MatrixRun waitForCompletion(MatrixBuildExecution exec, MatrixConfiguration c) throws InterruptedException, IOException { - BuildListener listener = exec.getListener(); - String whyInQueue = ""; - long startTime = System.currentTimeMillis(); - - // wait for the completion - int appearsCancelledCount = 0; - while(true) { - MatrixRun b = c.getBuildByNumber(exec.getBuild().getNumber()); - - // two ways to get beyond this. one is that the build starts and gets done, - // or the build gets cancelled before it even started. - if(b!=null && !b.isBuilding()) { - Result buildResult = b.getResult(); - if(buildResult!=null) - return b; - } - Queue.Item qi = c.getQueueItem(); - if(b==null && qi==null) - appearsCancelledCount++; - else - appearsCancelledCount = 0; - - if(appearsCancelledCount>=5) { - // there's conceivably a race condition in computating b and qi, as their computation - // are not synchronized. There are indeed several reports of Hudson incorrectly assuming - // builds being cancelled. See - // http://www.nabble.com/Master-slave-problem-tt14710987.html and also - // http://www.nabble.com/Anyone-using-AccuRev-plugin--tt21634577.html#a21671389 - // because of this, we really make sure that the build is cancelled by doing this 5 - // times over 5 seconds - listener.getLogger().println(Messages.MatrixBuild_AppearsCancelled(ModelHyperlinkNote.encodeTo(c))); - return null; - } - - if(qi!=null) { - // if the build seems to be stuck in the queue, display why - String why = qi.getWhy(); - if(why != null && !why.equals(whyInQueue) && System.currentTimeMillis()-startTime>5000) { - listener.getLogger().print("Configuration " + ModelHyperlinkNote.encodeTo(c)+" is still in the queue: "); - qi.getCauseOfBlockage().print(listener); //this is still shown on the same line - whyInQueue = why; - } - } - - Thread.sleep(1000); - } - } - - @Extension - public static class DescriptorImpl extends MatrixExecutionStrategyDescriptor { - @Override - public String getDisplayName() { - return "Classic"; - } - } -} diff --git a/core/src/main/java/hudson/matrix/JDKAxis.java b/core/src/main/java/hudson/matrix/JDKAxis.java deleted file mode 100644 index fecd908deef180466e44601dbf2ce40e247bfb04..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/JDKAxis.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2010, InfraDNA, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.Extension; -import jenkins.model.Jenkins; -import org.kohsuke.stapler.DataBoundConstructor; - -import java.util.Arrays; -import java.util.List; - -/** - * {@link Axis} that selects available JDKs. - * - * @author Kohsuke Kawaguchi - */ -public class JDKAxis extends Axis { - /** - * JDK axis was used to be stored as a plain "Axis" with the name "jdk", - * so it cannot be configured by any other name. - */ - public JDKAxis(List values) { - super("jdk", values); - } - - @DataBoundConstructor - public JDKAxis(String[] values) { - super("jdk", Arrays.asList(values)); - } - - @Override - public boolean isSystem() { - return true; - } - - @Extension - public static class DescriptorImpl extends AxisDescriptor { - @Override - public String getDisplayName() { - return Messages.JDKAxis_DisplayName(); - } - - /** - * If there's no JDK configured, there's no point in this axis. - */ - @Override - public boolean isInstantiable() { - return !Jenkins.getInstance().getJDKs().isEmpty(); - } - } -} diff --git a/core/src/main/java/hudson/matrix/LabelAxis.java b/core/src/main/java/hudson/matrix/LabelAxis.java deleted file mode 100644 index bd32df00371af045d80a0ce4637cf9f496d890c7..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/LabelAxis.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2010, InfraDNA, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.Extension; -import hudson.Functions; -import hudson.Util; -import jenkins.model.Jenkins; -import hudson.model.labels.LabelAtom; -import org.kohsuke.stapler.DataBoundConstructor; - -import java.util.List; - -/** - * {@link Axis} that selects label expressions. - * - * @author Kohsuke Kawaguchi - */ -public class LabelAxis extends Axis { - @DataBoundConstructor - public LabelAxis(String name, List values) { - super(name, values); - } - - @Override - public boolean isSystem() { - return true; - } - - @Override - public String getValueString() { - return Util.join(getValues(),"/"); - } - - @Extension - public static class DescriptorImpl extends AxisDescriptor { - @Override - public String getDisplayName() { - return Messages.LabelAxis_DisplayName(); - } - - /** - * If there's no distributed build set up, it's pointless to provide this axis. - */ - @Override - public boolean isInstantiable() { - Jenkins h = Jenkins.getInstance(); - return !h.getNodes().isEmpty() || !h.clouds.isEmpty(); - } - - private String jsstr(String body, Object... args) { - return '\"'+Functions.jsStringEscape(String.format(body,args))+'\"'; - } - - public String buildLabelCheckBox(LabelAtom la, LabelAxis instance) { - return jsstr("", - la.getName(),la.getDescription()); - // '${h.jsStringEscape('')}' - } - } -} diff --git a/core/src/main/java/hudson/matrix/LabelExpAxis.java b/core/src/main/java/hudson/matrix/LabelExpAxis.java deleted file mode 100644 index 281438d838210573b5229d4ca1d54bf62ebb003e..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/LabelExpAxis.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2010, InfraDNA, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.Extension; -import hudson.Util; -import jenkins.model.Jenkins; - -import java.util.LinkedList; -import java.util.List; - -import org.kohsuke.stapler.DataBoundConstructor; - -/** - * {@link Axis} that selects label expressions. - * - * @since 1.403 - */ -public class LabelExpAxis extends Axis { - - @DataBoundConstructor - public LabelExpAxis(String name, String values) { - super(name, getExprValues(values)); - } - - public LabelExpAxis(String name, List values) { - super(name, values); - } - - @Override - public boolean isSystem() { - return true; - } - - @Override - public DescriptorImpl getDescriptor() { - return (DescriptorImpl) Jenkins.getInstance().getDescriptorOrDie(getClass()); - } - - public String getValuesString(){ - StringBuffer sb = new StringBuffer(); - for(String item: this.getValues()){ - sb.append(item); - sb.append("\n"); - } - return sb.toString(); - } - - @Extension - public static class DescriptorImpl extends AxisDescriptor { - @Override - public String getDisplayName() { - return Messages.LabelExpAxis_DisplayName(); - } - - /** - * If there's no distributed build set up, it's pointless to provide this axis. - */ - @Override - public boolean isInstantiable() { - Jenkins h = Jenkins.getInstance(); - return !h.getNodes().isEmpty() || !h.clouds.isEmpty(); - } - } - - public static List getExprValues(String valuesString){ - List expressions = new LinkedList(); - String[] exprs = valuesString.split("\n"); - for(String expr: exprs){ - expressions.add(Util.fixEmptyAndTrim(expr)); - } - return expressions; - } - -} - diff --git a/core/src/main/java/hudson/matrix/Layouter.java b/core/src/main/java/hudson/matrix/Layouter.java deleted file mode 100644 index 051b8f6239166261f21adab3aacfa7a21a371169..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/Layouter.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import java.util.List; -import java.util.ArrayList; -import java.util.AbstractList; -import java.util.Map; -import java.util.HashMap; - -/** - * Used to assist thegeneration of config table. - * - *

- * {@link Axis Axes} are split into four groups. - * {@link #x Ones that are displayed as columns}, - * {@link #y Ones that are displayed as rows}, - * {@link #z Ones that are listed as bullet items inside table cell}, - * and those which only have one value, and therefore doesn't show up - * in the table. - * - *

- * Because of object reuse inside {@link Layouter}, this class is not thread-safe. - * - * @author Kohsuke Kawaguchi - */ -public abstract class Layouter { - public final List x,y,z; - /** - * Axes that only have one value. - */ - private final List trivial = new ArrayList(); - /** - * Number of data columns and rows. - */ - private int xSize, ySize, zSize; - - - public Layouter(List x, List y, List z) { - this.x = x; - this.y = y; - this.z = z; - init(); - } - - /** - * Automatically split axes to x,y, and z. - */ - public Layouter(AxisList axisList) { - x = new ArrayList(); - y = new ArrayList(); - z = new ArrayList(); - - List nonTrivialAxes = new ArrayList(); - for (Axis a : axisList) { - if(a.size()>1) - nonTrivialAxes.add(a); - else - trivial.add(a); - } - - switch(nonTrivialAxes.size()) { - case 0: - break; - case 1: - z.add(nonTrivialAxes.get(0)); - break; - case 2: - // use the longer axis in Y - Axis a = nonTrivialAxes.get(0); - Axis b = nonTrivialAxes.get(1); - x.add(a.size() > b.size() ? b : a); - y.add(a.size() > b.size() ? a : b); - break; - default: - // for size > 3, use x and y, and try to pack y more - for( int i=0; i=0; n-- ) - w *= x.get(n).size(); - return w; - } - - /** - * Computes the width of n-th Y-axis. - */ - public int height(int n) { - return calc(y,n); - } - - private int calc(List l, int n) { - int w = 1; - for( n++ ; n getRows() { - return new AbstractList() { - final Row row = new Row(); - public Row get(int index) { - row.index = index; - return row; - } - - public int size() { - return ySize; - } - }; - } - - /** - * Represents a row, which is a collection of {@link Column}s. - */ - public final class Row extends AbstractList { - private int index; - final Column col = new Column(); - - @Override - public Column get(int index) { - col.xp = index; - col.yp = Row.this.index; - return col; - } - - @Override - public int size() { - return xSize; - } - - public String drawYHeader(int n) { - int base = calc(y,n); - if(index/base==(index-1)/base && index!=0) return null; // no need to draw a new value - - Axis axis = y.get(n); - return axis.value((index/base)%axis.getValues().size()); - } - } - - protected abstract T getT(Combination c); - - public final class Column extends AbstractList { - /** - * Cell position. - */ - private int xp,yp; - - private final Map m = new HashMap(); - - public T get(int zp) { - m.clear(); - buildMap(xp,x); - buildMap(yp,y); - buildMap(zp,z); - for (Axis a : trivial) { - if (a.size() > 0) { - m.put(a.name, a.value(0)); - } - } - return getT(new Combination(m)); - } - - private void buildMap(int p, List axes) { - int n = p; - for( int i= axes.size()-1; i>=0; i-- ) { - Axis a = axes.get(i); - m.put(a.name, a.value(n%a.size())); - n /= a.size(); - } - } - - public int size() { - return zSize; - } - } -} diff --git a/core/src/main/java/hudson/matrix/LinkedLogRotator.java b/core/src/main/java/hudson/matrix/LinkedLogRotator.java deleted file mode 100644 index c46870318e1461d2e297f8048047cd0ff97cd4e7..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/LinkedLogRotator.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.model.Job; -import hudson.tasks.LogRotator; - -import java.io.IOException; -import java.util.logging.Logger; - -/** - * {@link LogRotator} for {@link MatrixConfiguration}, - * which discards the builds if and only if it's discarded - * in the parent. - * - *

- * Because of the serialization compatibility, we can't easily - * refactor {@link LogRotator} into a contract and an implementation. - * - * @author Kohsuke Kawaguchi - */ -final class LinkedLogRotator extends LogRotator { - LinkedLogRotator(int artifactDaysToKeep, int artifactNumToKeep) { - super(-1, -1, artifactDaysToKeep, artifactNumToKeep); - } - - /** - * @deprecated since 1.369 - * Use {@link #LinkedLogRotator(int, int)} - */ - LinkedLogRotator() { - super(-1, -1, -1, -1); - } - - @Override - public void perform(Job _job) throws IOException, InterruptedException { - // Let superclass handle clearing artifacts, if configured: - super.perform(_job); - MatrixConfiguration job = (MatrixConfiguration) _job; - - // copy it to the array because we'll be deleting builds as we go. - for( MatrixRun r : job.getBuilds().toArray(new MatrixRun[0]) ) { - if(job.getParent().getBuildByNumber(r.getNumber())==null) { - LOGGER.fine("Deleting "+r.getFullDisplayName()); - r.delete(); - } - } - - if(!job.isActiveConfiguration() && job.getLastBuild()==null) { - LOGGER.fine("Deleting "+job.getFullDisplayName()+" because the configuration is inactive and there's no builds"); - job.delete(); - } - } - - private static final Logger LOGGER = Logger.getLogger(LinkedLogRotator.class.getName()); -} diff --git a/core/src/main/java/hudson/matrix/MatrixAggregatable.java b/core/src/main/java/hudson/matrix/MatrixAggregatable.java deleted file mode 100644 index 5074988d83721dea1c0b829ad63eb455f8089cf4..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/MatrixAggregatable.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.model.JobProperty; -import hudson.tasks.BuildWrapper; -import hudson.tasks.Publisher; -import hudson.ExtensionPoint; -import hudson.Launcher; -import hudson.model.BuildListener; - -/** - * {@link Publisher}, {@link JobProperty}, {@link BuildWrapper} can optionally implement this interface - * to perform result aggregation across {@link MatrixRun}. - * - *

- * This is useful for example to aggregate all the test results - * in {@link MatrixRun} into a single table/graph. - * - * @author Kohsuke Kawaguchi - * @since 1.115 - */ -public interface MatrixAggregatable extends ExtensionPoint { - /** - * Creates a new instance of the aggregator. - * - *

- * This method is called during the build of - * {@link MatrixBuild} and the created aggregator - * will perform the aggregation. - * - * @param build - * The build for which the aggregation shall happen. Never null. - * @param launcher - * Can be used to launch processes during the build. - * @param listener - * Progress report and errors during the aggregation should - * be sent to this object. Never null. - * - * @return - * null if the implementation is not willing to contribute - * an aggregator. - * - * @see MatrixAggregator#build - * @see MatrixAggregator#listener - */ - MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener); -} diff --git a/core/src/main/java/hudson/matrix/MatrixAggregator.java b/core/src/main/java/hudson/matrix/MatrixAggregator.java deleted file mode 100644 index dbdfcaffa792da9e22139a0fb350aa1cd5866901..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/MatrixAggregator.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.ExtensionPoint; -import hudson.Launcher; -import hudson.model.BuildListener; -import hudson.tasks.BuildStep; -import hudson.tasks.Publisher; - -import java.io.IOException; - -/** - * Performs the aggregation of {@link MatrixRun} results - * into {@link MatrixBuild}. - * - *

- * {@link MatrixAggregator} is a transitive stateful mutable object. - * Unlike {@link Publisher}, it is not persisted. Instead, a fresh - * instance is created for each {@link MatrixBuild}, and various - * methods on this class are invoked in the event callback style - * as the build progresses. - * - *

- * The end result of the aggregation should be - * {@link MatrixBuild#addAction(Action) contributed as actions}. - * - * @author Kohsuke Kawaguchi - * @since 1.115 - * @see MatrixAggregatable - */ -public abstract class MatrixAggregator implements ExtensionPoint { - /** - * The build in progress. Never null. - */ - protected final MatrixBuild build; - - protected final Launcher launcher; - /** - * The listener to send the output to. Never null. - */ - protected final BuildListener listener; - - protected MatrixAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) { - this.build = build; - this.launcher = launcher; - this.listener = listener; - } - - /** - * Called before the build starts. - * - * @return - * true if the build can continue, false if there was an error - * and the build needs to be aborted. - * @see BuildStep#prebuild(AbstractBuild,BuildListener) - */ - public boolean startBuild() throws InterruptedException, IOException { - return true; - } - - /** - * Called whenever one run is completed. - * - * @param run - * The completed {@link MatrixRun} object. Always non-null. - * @return - * See {@link #startBuild()} for the return value semantics. - */ - public boolean endRun(MatrixRun run) throws InterruptedException, IOException { - return true; - } - - /** - * Called after all the {@link MatrixRun}s have been completed - * to indicate that the build is about to finish. - * - * @return - * See {@link #startBuild()} for the return value semantics. - */ - public boolean endBuild() throws InterruptedException, IOException { - return true; - } -} diff --git a/core/src/main/java/hudson/matrix/MatrixBuild.java b/core/src/main/java/hudson/matrix/MatrixBuild.java deleted file mode 100644 index 5675842311e07abda3f760a8613f8272136d0563..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/MatrixBuild.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, - * Red Hat, Inc., Tom Huybrechts - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.AbortException; -import hudson.Functions; -import hudson.Util; -import hudson.console.ModelHyperlinkNote; -import hudson.matrix.MatrixConfiguration.ParentBuildAction; -import hudson.model.AbstractBuild; -import hudson.model.AbstractProject; -import hudson.model.BuildListener; -import hudson.model.Executor; -import hudson.model.Fingerprint; -import hudson.model.Queue; -import hudson.model.Queue.Item; -import hudson.model.Result; -import hudson.util.HttpResponses; -import jenkins.model.Jenkins; -import org.kohsuke.stapler.Stapler; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.export.Exported; -import org.kohsuke.stapler.interceptor.RequirePOST; - -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import javax.servlet.ServletException; - -/** - * Build of {@link MatrixProject}. - * - * @author Kohsuke Kawaguchi - */ -public class MatrixBuild extends AbstractBuild { - private AxisList axes; - - /** - * If non-null, the {@link MatrixBuild} originates from the given build number. - */ - private Integer baseBuild; - - public MatrixBuild(MatrixProject job) throws IOException { - super(job); - } - - public MatrixBuild(MatrixProject job, Calendar timestamp) { - super(job, timestamp); - } - - public MatrixBuild(MatrixProject project, File buildDir) throws IOException { - super(project, buildDir); - } - - public Object readResolve() { - // MatrixBuild.axes added in 1.285; default to parent axes for old data - if (axes==null) - axes = getParent().getAxes(); - return this; - } - - /** - * Deletes the build and all matrix configurations in this build when the button is pressed. - */ - @RequirePOST - public void doDoDeleteAll( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { - checkPermission(DELETE); - - // We should not simply delete the build if it has been explicitly - // marked to be preserved, or if the build should not be deleted - // due to dependencies! - String why = getWhyKeepLog(); - if (why!=null) { - sendError(hudson.model.Messages.Run_UnableToDelete(toString(),why),req,rsp); - return; - } - - List runs = getExactRuns(); - for(MatrixRun run : runs){ - why = run.getWhyKeepLog(); - if (why!=null) { - sendError(hudson.model.Messages.Run_UnableToDelete(toString(),why),req,rsp); - return; - } - run.delete(); - } - delete(); - rsp.sendRedirect2(req.getContextPath()+'/' + getParent().getUrl()); - } - - - /** - * Used by view to render a ball for {@link MatrixRun}. - */ - public final class RunPtr { - public final Combination combination; - private RunPtr(Combination c) { this.combination=c; } - - public MatrixRun getRun() { - return MatrixBuild.this.getRun(combination); - } - - /** - * Return the URL to the run that this pointer references. - * - * In the typical case, this creates {@linkplain #getShortUrl() a very short relative url}. - * If the referenced run is a nearest previous build, this method returns a longer URL to that exact build. - * {@link MatrixRun} which belongs to a given build {@link MatrixBuild}. - * If there is no run which belongs to the build, return url of run, which belongs to the nearest previous build. - */ - public String getNearestRunUrl() { - MatrixRun r = getRun(); - if (r==null) return null; - if (getNumber()==r.getNumber()) - return getShortUrl()+'/'; - else - return Stapler.getCurrentRequest().getContextPath()+'/'+r.getUrl(); - } - - public String getShortUrl() { - return Util.rawEncode(combination.toString()); - } - - public String getTooltip() { - MatrixRun r = getRun(); - if(r!=null) return r.getIconColor().getDescription(); - Queue.Item item = Jenkins.getInstance().getQueue().getItem(getParent().getItem(combination)); - if(item!=null) - return item.getWhy(); - return null; // fall back - } - } - - public Layouter getLayouter() { - // axes can be null if build page is access right when build starts - return axes == null ? null : new Layouter(axes) { - protected RunPtr getT(Combination c) { - return new RunPtr(c); - } - }; - } - - /** - * Sets the base build from which this build is derived. - * @since 1.416 - */ - public void setBaseBuild(MatrixBuild baseBuild) { - this.baseBuild = (baseBuild==null || baseBuild==getPreviousBuild()) ? null : baseBuild.getNumber(); - } - - /** - * Returns the base {@link MatrixBuild} that this build originates from. - *

- * If this build is a partial build, unexecuted {@link MatrixRun}s are delegated to this build number. - */ - public MatrixBuild getBaseBuild() { - return baseBuild==null ? getPreviousBuild() : getParent().getBuildByNumber(baseBuild); - } - - /** - * Gets the {@link MatrixRun} in this build that corresponds - * to the given combination. - */ - public MatrixRun getRun(Combination c) { - MatrixConfiguration config = getParent().getItem(c); - if(config==null) return null; - return getRunForConfiguration(config); - } - - /** - * Like {@link #getRun(Combination)}, but do not approximate the result by earlier execution - * of the given combination (which is done for partial rebuild of the matrix.) - */ - public MatrixRun getExactRun(Combination c) { - MatrixConfiguration config = getParent().getItem(c); - if(config==null) return null; - return config.getBuildByNumber(getNumber()); - } - - /** - * Returns all {@link MatrixRun}s for this {@link MatrixBuild}. - */ - @Exported - public List getRuns() { - List r = new ArrayList(); - for(MatrixConfiguration c : getParent().getItems()) { - MatrixRun b = getRunForConfiguration(c); - if (b != null) r.add(b); - } - return r; - } - - private MatrixRun getRunForConfiguration(MatrixConfiguration c) { - for (MatrixBuild b=this; b!=null; b=b.getBaseBuild()) { - MatrixRun r = c.getBuildByNumber(b.getNumber()); - if (r!=null) return r; - } - return null; - } - - /** - * Returns all {@link MatrixRun}s for exactly this {@link MatrixBuild}. - *

- * Unlike {@link #getRuns()}, this method excludes those runs - * that didn't run and got inherited. - * @since 1.413 - */ - public List getExactRuns() { - List r = new ArrayList(); - for(MatrixConfiguration c : getParent().getItems()) { - MatrixRun b = c.getBuildByNumber(getNumber()); - if (b != null) r.add(b); - } - return r; - } - - @Override - public String getWhyKeepLog() { - MatrixBuild b = getNextBuild(); - if (isLinkedBy(b)) - return Messages.MatrixBuild_depends_on_this(b.getDisplayName()); - return super.getWhyKeepLog(); - } - - /** - * @return True if another {@link MatrixBuild} build (passed as a parameter) depends on this build. - * @since 1.481 - */ - public boolean isLinkedBy(MatrixBuild b) { - if(null == b) - return false; - for(MatrixConfiguration c : b.getParent().getActiveConfigurations()) { - MatrixRun r = c.getNearestOldBuild(b.getNumber()); - if (r != null && r.getNumber()==getNumber()) - return true; - } - return false; - } - - /** - * True if this build didn't do a full build and it is depending on the result of the previous build. - */ - public boolean isPartial() { - for(MatrixConfiguration c : getParent().getActiveConfigurations()) { - MatrixRun b = c.getNearestOldBuild(getNumber()); - if (b != null && b.getNumber()!=getNumber()) - return true; - } - return false; - } - - @Override - public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { - try { - MatrixRun item = getRun(Combination.fromString(token)); - if(item!=null) { - if (item.getNumber()==this.getNumber()) - return item; - else { - // redirect the user to the correct URL - String url = Functions.joinPath(item.getUrl(), req.getRestOfPath()); - String qs = req.getQueryString(); - if (qs!=null) url+='?'+qs; - throw HttpResponses.redirectViaContextPath(url); - } - } - } catch (IllegalArgumentException _) { - // failed to parse the token as Combination. Must be something else - } - return super.getDynamic(token,req,rsp); - } - - @Override - public void run() { - execute(new MatrixBuildExecution()); - } - - @Override - public Fingerprint.RangeSet getDownstreamRelationship(AbstractProject that) { - Fingerprint.RangeSet rs = super.getDownstreamRelationship(that); - for(MatrixRun run : getRuns()) - rs.add(run.getDownstreamRelationship(that)); - return rs; - } - - /** - * Object that lives from the start of {@link MatrixBuild} execution to its end. - * - * Used to keep track of things that are needed only during the build. - */ - public class MatrixBuildExecution extends AbstractBuildExecution { - private final List aggregators = new ArrayList(); - private Set activeConfigurations; - - /** - * Snapshot of {@link MatrixProject#getActiveConfigurations()} to ensure - * that the build will use a consistent view of it. - */ - public Set getActiveConfigurations() { - return activeConfigurations; - } - - /** - * Aggregators attached to this build execution, that are notified - * of every start/end of {@link MatrixRun}. - */ - public List getAggregators() { - return aggregators; - } - - protected Result doRun(BuildListener listener) throws Exception { - MatrixProject p = getProject(); - PrintStream logger = listener.getLogger(); - - // give axes a chance to rebuild themselves - activeConfigurations = p.rebuildConfigurations(this); - - // list up aggregators - listUpAggregators(p.getPublishers().values()); - listUpAggregators(p.getProperties().values()); - listUpAggregators(p.getBuildWrappers().values()); - - axes = p.getAxes(); - - try { - return p.getExecutionStrategy().run(this); - } catch( InterruptedException e ) { - logger.println("Aborted"); - Executor x = Executor.currentExecutor(); - x.recordCauseOfInterruption(MatrixBuild.this, listener); - return x.abortResult(); - } catch (AbortException e) { - logger.println(e.getMessage()); - return Result.FAILURE; - } finally { - // if the build was aborted in the middle. Cancel all the configuration builds. - Queue q = Jenkins.getInstance().getQueue(); - synchronized(q) {// avoid micro-locking in q.cancel. - final int n = getNumber(); - for (MatrixConfiguration c : activeConfigurations) { - for (Item i : q.getItems(c)) { - ParentBuildAction a = i.getAction(ParentBuildAction.class); - if (a!=null && a.parent==getBuild()) { - q.cancel(i); - logger.println(Messages.MatrixBuild_Cancelled(ModelHyperlinkNote.encodeTo(c))); - } - } - MatrixRun b = c.getBuildByNumber(n); - if(b!=null && b.isBuilding()) {// executor can spend some time in post production state, so only cancel in-progress builds. - Executor exe = b.getExecutor(); - if(exe!=null) { - logger.println(Messages.MatrixBuild_Interrupting(ModelHyperlinkNote.encodeTo(b))); - exe.interrupt(); - } - } - } - } - } - } - - private void listUpAggregators(Collection values) { - for (Object v : values) { - if (v instanceof MatrixAggregatable) { - MatrixAggregatable ma = (MatrixAggregatable) v; - MatrixAggregator a = ma.createAggregator(MatrixBuild.this, launcher, listener); - if(a!=null) - aggregators.add(a); - } - } - } - - public void post2(BuildListener listener) throws Exception { - for (MatrixAggregator a : aggregators) - a.endBuild(); - } - } -} diff --git a/core/src/main/java/hudson/matrix/MatrixChildAction.java b/core/src/main/java/hudson/matrix/MatrixChildAction.java deleted file mode 100644 index a43e59d932e65cdf7fa336d4051fb484d4a2f897..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/MatrixChildAction.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2012, Chris Johnson - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.model.Action; -import hudson.model.Queue; -import hudson.model.Queue.Task; - -/** - * Optional interface for {@link Action}s that are used as parameters - * to {@link Queue#schedule(Task, int, Action...)} to indicate that they - * want to be also passed to the {@link MatrixRun}s from its parent {@link MatrixBuild}. - * - * @author Chris Johnson - * @since 1.481 - */ -public interface MatrixChildAction extends Action { -} diff --git a/core/src/main/java/hudson/matrix/MatrixConfiguration.java b/core/src/main/java/hudson/matrix/MatrixConfiguration.java deleted file mode 100644 index f57fff9682d52e6d8cdd245f43c04e077eaf8553..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/MatrixConfiguration.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Tom Huybrechts - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.EnvVars; -import hudson.Util; -import hudson.model.Action; -import hudson.model.Executor; -import hudson.model.InvisibleAction; -import hudson.model.Node; -import hudson.model.Queue.QueueAction; -import hudson.model.TaskListener; -import hudson.util.AlternativeUiTextProvider; -import hudson.util.DescribableList; -import hudson.model.AbstractBuild; -import hudson.model.Cause; -import hudson.model.CauseAction; -import hudson.model.DependencyGraph; -import hudson.model.Descriptor; -import jenkins.model.BuildDiscarder; -import jenkins.model.Jenkins; -import hudson.model.Item; -import hudson.model.ItemGroup; -import hudson.model.JDK; -import hudson.model.Label; -import hudson.model.ParametersAction; -import hudson.model.Project; -import hudson.model.SCMedItem; -import hudson.model.Queue.NonBlockingTask; -import hudson.model.Cause.LegacyCodeCause; -import hudson.scm.SCM; -import jenkins.scm.SCMCheckoutStrategy; -import hudson.tasks.BuildWrapper; -import hudson.tasks.Builder; -import hudson.tasks.LogRotator; -import hudson.tasks.Publisher; - -import java.io.IOException; -import java.util.Collections; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * One configuration of {@link MatrixProject}. - * - * @author Kohsuke Kawaguchi - */ -public class MatrixConfiguration extends Project implements SCMedItem, NonBlockingTask { - /** - * The actual value combination. - */ - private transient /*final*/ Combination combination; - - /** - * Hash value of {@link #combination}. Cached for efficiency. - */ - private transient String digestName; - - public MatrixConfiguration(MatrixProject parent, Combination c) { - super(parent,c.toString()); - setCombination(c); - } - - @Override - public void onLoad(ItemGroup parent, String name) throws IOException { - // directory name is not a name for us --- it's taken from the combination name - super.onLoad(parent, combination.toString()); - } - - @Override - public EnvVars getEnvironment(Node node, TaskListener listener) throws IOException, InterruptedException { - EnvVars env = super.getEnvironment(node, listener); - - AxisList axes = getParent().getAxes(); - for (Map.Entry e : getCombination().entrySet()) { - Axis a = axes.find(e.getKey()); - if (a!=null) - a.addBuildVariable(e.getValue(),env); // TODO: hijacking addBuildVariable but perhaps we need addEnvVar? - else - env.put(e.getKey(), e.getValue()); - } - - return env; - } - - @Override - protected void updateTransientActions(){ - // This method is exactly the same as in {@link #AbstractProject}. - // Enabling to call this method from MatrixProject is the only reason for overriding. - super.updateTransientActions(); - } - - @Override - public boolean isConcurrentBuild() { - return getParent().isConcurrentBuild(); - } - - @Override - public void setConcurrentBuild(boolean b) throws IOException { - throw new UnsupportedOperationException("The setting can be only changed at MatrixProject"); - } - - /** - * Used during loading to set the combination back. - */ - /*package*/ void setCombination(Combination c) { - this.combination = c; - this.digestName = c.digest().substring(0,8); - } - - /** - * Build numbers are always synchronized with the parent. - * - *

- * Computing this is bit tricky. Several considerations: - * - *

    - *
  1. A new configuration build #N is started while the parent build #N is building, - * and when that happens we want to return N. - *
  2. But the configuration build #N is done before the parent build #N finishes, - * and when that happens we want to return N+1 because that's going to be the next one. - *
  3. Configuration builds might skip some numbers if the parent build is aborted - * before this configuration is built. - *
  4. If nothing is building right now and the last build of the parent is #N, - * then we want to return N+1. - *
- */ - @Override - public int getNextBuildNumber() { - AbstractBuild lb = getParent().getLastBuild(); - - while (lb!=null && lb.isBuilding()) { - lb = lb.getPreviousBuild(); - } - if(lb==null) return 0; - - int n=lb.getNumber()+1; - - lb = getLastBuild(); - if(lb!=null) - n = Math.max(n,lb.getNumber()+1); - - return n; - } - - @Override - public int assignBuildNumber() throws IOException { - int nb = getNextBuildNumber(); - MatrixRun r = getLastBuild(); - if(r!=null && r.getNumber()>=nb) // make sure we don't schedule the same build twice - throw new IllegalStateException("Build #"+nb+" is already completed"); - return nb; - } - - @Override - public String getDisplayName() { - return combination.toCompactString(getParent().getAxes()); - } - - @Override - public MatrixProject getParent() { - return (MatrixProject)super.getParent(); - } - - /** - * Get the actual combination of the axes values for this {@link MatrixConfiguration} - */ - public Combination getCombination() { - return combination; - } - - /** - * Since {@link MatrixConfiguration} is always invoked from {@link MatrixRun} - * once and just once, there's no point in having a quiet period. - */ - @Override - public int getQuietPeriod() { - return 0; - } - - /** - * Inherit the value from the parent. - */ - @Override - public int getScmCheckoutRetryCount() { - return getParent().getScmCheckoutRetryCount(); - } - - /** - * Inherit the value from the parent. - */ - @Override - public SCMCheckoutStrategy getScmCheckoutStrategy() { - return getParent().getScmCheckoutStrategy(); - } - - @Override - public boolean isConfigurable() { - return false; - } - - @Override - protected Class getBuildClass() { - return MatrixRun.class; - } - - @Override - protected MatrixRun newBuild() throws IOException { - List actions = Executor.currentExecutor().getCurrentWorkUnit().context.actions; - MatrixBuild lb = getParent().getLastBuild(); - for (Action a : actions) { - if (a instanceof ParentBuildAction) { - MatrixBuild _lb = ((ParentBuildAction) a).parent; - if (_lb != null) { - lb = _lb; - } - } - } - - if (lb == null) { - throw new IOException("cannot start a build of " + getFullName() + " since its parent has no builds at all"); - } - - // for every MatrixRun there should be a parent MatrixBuild - MatrixRun lastBuild = new MatrixRun(this, lb.getTimestamp()); - - lastBuild.number = lb.getNumber(); - - builds.put(lastBuild); - return lastBuild; - } - - @Override - protected void buildDependencyGraph(DependencyGraph graph) { - } - - @Override - public MatrixConfiguration asProject() { - return this; - } - - @Override - public Label getAssignedLabel() { - // combine all the label axes by &&. - String expr; - String exprSlave = Util.join(combination.values(getParent().getAxes().subList(LabelAxis.class)), "&&"); - String exprLabel = Util.join(combination.values(getParent().getAxes().subList(LabelExpAxis.class)), "&&"); - if(!exprSlave.equals("") && !exprLabel.equals("")){ - expr = exprSlave + "&&" + exprLabel; - } else{ - expr = (exprSlave.equals("")) ? exprLabel : exprSlave; - } - return Jenkins.getInstance().getLabel(Util.fixEmpty(expr)); - } - - @Override - public String getPronoun() { - return AlternativeUiTextProvider.get(PRONOUN, this, Messages.MatrixConfiguration_Pronoun()); - } - - @Override - public JDK getJDK() { - return Jenkins.getInstance().getJDK(combination.get("jdk")); - } - -// -// inherit build setting from the parent project -// - @Override - public List getBuilders() { - return getParent().getBuilders(); - } - - @Override - public Map, Publisher> getPublishers() { - return getParent().getPublishers(); - } - - @Override - public DescribableList> getBuildersList() { - return getParent().getBuildersList(); - } - - @Override - public DescribableList> getPublishersList() { - return getParent().getPublishersList(); - } - - @Override - public Map, BuildWrapper> getBuildWrappers() { - return getParent().getBuildWrappers(); - } - - @Override - public DescribableList> getBuildWrappersList() { - return getParent().getBuildWrappersList(); - } - - @Override - public Publisher getPublisher(Descriptor descriptor) { - return getParent().getPublisher(descriptor); - } - - @Override - public BuildDiscarder getBuildDiscarder() { - // TODO: LinkedLogRotator doesn't work very well in the face of pluggable BuildDiscarder but I don't know what to do - BuildDiscarder bd = getParent().getBuildDiscarder(); - if (bd instanceof LogRotator) { - LogRotator lr = (LogRotator) bd; - return new LinkedLogRotator(lr.getArtifactDaysToKeep(),lr.getArtifactNumToKeep()); - } - return new LinkedLogRotator(); - } - - @Override - public SCM getScm() { - return getParent().getScm(); - } - - /*package*/ String getDigestName() { - return digestName; - } - - /** - * JDK cannot be set on {@link MatrixConfiguration} because - * it's controlled by {@link MatrixProject}. - * @deprecated - * Not supported. - */ - @Override - public void setJDK(JDK jdk) throws IOException { - throw new UnsupportedOperationException(); - } - - /** - * @deprecated - * Value is controlled by {@link MatrixProject}. - */ - @Override - public void setBuildDiscarder(BuildDiscarder logRotator) { - throw new UnsupportedOperationException(); - } - - /** - * Returns true if this configuration is a configuration - * currently in use today (as opposed to the ones that are - * there only to keep the past record.) - * - * @see MatrixProject#getActiveConfigurations() - */ - public boolean isActiveConfiguration() { - return getParent().getActiveConfigurations().contains(this); - } - - /** - * On Cygwin, path names cannot be longer than 256 chars. - * See http://cygwin.com/ml/cygwin/2005-04/msg00395.html and - * http://www.nabble.com/Windows-Filename-too-long-errors-t3161089.html for - * the background of this issue. Setting this flag to true would - * cause Jenkins to use cryptic but short path name, giving more room for - * jobs to use longer path names. - */ - public static boolean useShortWorkspaceName = Boolean.getBoolean(MatrixConfiguration.class.getName()+".useShortWorkspaceName"); - - /** - * @deprecated - * Use {@link #scheduleBuild(ParametersAction, Cause)}. Since 1.283 - */ - public boolean scheduleBuild(ParametersAction parameters) { - return scheduleBuild(parameters, new LegacyCodeCause()); - } - - /** Starts the build with the ParametersAction that are passed in. - * - * @param parameters - * Can be null. - * @deprecated - * Use {@link #scheduleBuild(List, Cause)}. Since 1.480 - */ - public boolean scheduleBuild(ParametersAction parameters, Cause c) { - return scheduleBuild(Collections.singletonList(parameters), c); - } - /** - * Starts the build with the actions that are passed in. - * - * @param actions Can be null. - * @param c Reason for starting the build - */ - public boolean scheduleBuild(List actions, Cause c) { - List allActions = new ArrayList(); - - if(actions != null) - allActions.addAll(actions); - - allActions.add(new ParentBuildAction()); - allActions.add(new CauseAction(c)); - - return Jenkins.getInstance().getQueue().schedule2(this, getQuietPeriod(), allActions ).isAccepted(); - } - - /** - * - */ - public static class ParentBuildAction extends InvisibleAction implements QueueAction { - public transient MatrixBuild parent = (MatrixBuild)Executor.currentExecutor().getCurrentExecutable(); - public boolean shouldSchedule(List actions) { - return true; - } - } -} diff --git a/core/src/main/java/hudson/matrix/MatrixConfigurationSorter.java b/core/src/main/java/hudson/matrix/MatrixConfigurationSorter.java deleted file mode 100644 index 5066b711e639245de504f7d7188fa66cc35898c4..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/MatrixConfigurationSorter.java +++ /dev/null @@ -1,36 +0,0 @@ -package hudson.matrix; - -import hudson.ExtensionPoint; -import hudson.model.AbstractDescribableImpl; -import hudson.util.FormValidation; - -import java.util.Comparator; - -/** - * Add sorting for configurations {@link MatrixConfiguration}s of matrix job {@link MatrixProject} - * - * @since 1.439 - * @author Lucie Votypkova - * @see MatrixConfigurationSorterDescriptor - */ -public abstract class MatrixConfigurationSorter extends AbstractDescribableImpl implements ExtensionPoint, Comparator { - - /** - * Checks if this sorter is properly configured and applicable for the given project. - * - *

- * This method is invoked when the configuration is submitted to ensure that the sorter is compatible - * with the current project configuration (most probably with its {@link Axis}.) - * - * @param p - * Project for which this sorter is being used for. - * @throws FormValidation - * If you need to report an error to the user and reject the form submission. - */ - public abstract void validate(MatrixProject p) throws FormValidation; - - @Override - public MatrixConfigurationSorterDescriptor getDescriptor() { - return (MatrixConfigurationSorterDescriptor)super.getDescriptor(); - } -} diff --git a/core/src/main/java/hudson/matrix/MatrixConfigurationSorterDescriptor.java b/core/src/main/java/hudson/matrix/MatrixConfigurationSorterDescriptor.java deleted file mode 100644 index e4c472622264026f336686935e3fd2c5029668ba..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/MatrixConfigurationSorterDescriptor.java +++ /dev/null @@ -1,27 +0,0 @@ -package hudson.matrix; - -import hudson.DescriptorExtensionList; -import hudson.model.Descriptor; -import jenkins.model.Jenkins; - -/** - * Descriptor for {@link MatrixConfigurationSorter}. - * - * @author Kohsuke Kawaguchi - * @since 1.439 - */ -public abstract class MatrixConfigurationSorterDescriptor extends Descriptor { - protected MatrixConfigurationSorterDescriptor(Class clazz) { - super(clazz); - } - - protected MatrixConfigurationSorterDescriptor() { - } - - /** - * Returns all the registered {@link MatrixConfigurationSorterDescriptor}s. - */ - public static DescriptorExtensionList all() { - return Jenkins.getInstance().getDescriptorList(MatrixConfigurationSorter.class); - } -} diff --git a/core/src/main/java/hudson/matrix/MatrixExecutionStrategy.java b/core/src/main/java/hudson/matrix/MatrixExecutionStrategy.java deleted file mode 100644 index 33948422d7bddb91edc2ebb9573bed4eaa82ca11..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/MatrixExecutionStrategy.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2012, CloudBees, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.ExtensionPoint; -import hudson.matrix.MatrixBuild.MatrixBuildExecution; -import hudson.model.AbstractDescribableImpl; -import hudson.model.BuildListener; -import hudson.model.Result; - -import java.io.IOException; -import java.util.List; - -/** - * Controls the execution sequence of {@link MatrixConfiguration} when {@link MatrixProject} builds, - * including what degree it gets serialized/parallelled, how the whole build is abandoned when - * some fails, etc. - * - * @author Kohsuke Kawaguchi - * @since 1.456 - */ -public abstract class MatrixExecutionStrategy extends AbstractDescribableImpl implements ExtensionPoint { - public Result run(MatrixBuildExecution execution) throws InterruptedException, IOException { - return run(execution.getBuild(), execution.getAggregators(), execution.getListener()); - } - - /** - * @deprecated - * Override {@link #run(MatrixBuild.MatrixBuildExecution)} - */ - public Result run(MatrixBuild build, List aggregators, BuildListener listener) throws InterruptedException, IOException { - throw new UnsupportedOperationException(getClass()+" needs to override run(MatrixBuildExecution)"); - } - - @Override - public MatrixExecutionStrategyDescriptor getDescriptor() { - return (MatrixExecutionStrategyDescriptor)super.getDescriptor(); - } -} diff --git a/core/src/main/java/hudson/matrix/MatrixExecutionStrategyDescriptor.java b/core/src/main/java/hudson/matrix/MatrixExecutionStrategyDescriptor.java deleted file mode 100644 index b554e5df96d9828498c39662f39edaa0d20141fa..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/MatrixExecutionStrategyDescriptor.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2012, CloudBees, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import hudson.DescriptorExtensionList; -import hudson.model.Descriptor; -import jenkins.model.Jenkins; - -/** - * @author Kohsuke Kawaguchi - * @since 1.456 - */ -public abstract class MatrixExecutionStrategyDescriptor extends Descriptor { - protected MatrixExecutionStrategyDescriptor(Class clazz) { - super(clazz); - } - - protected MatrixExecutionStrategyDescriptor() { - } - - /** - * Returns all the registered {@link MatrixExecutionStrategyDescriptor}s. - */ - public static DescriptorExtensionList all() { - return Jenkins.getInstance().getDescriptorList(MatrixExecutionStrategy.class); - } -} diff --git a/core/src/main/java/hudson/matrix/MatrixProject.java b/core/src/main/java/hudson/matrix/MatrixProject.java deleted file mode 100644 index 6390db6839c0900447e94099017d28fd5d359f41..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/MatrixProject.java +++ /dev/null @@ -1,951 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, - * Jorg Heymans, Red Hat, Inc., id:cactusman - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.matrix; - -import com.google.common.base.Function; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import hudson.CopyOnWrite; -import hudson.Extension; -import hudson.Util; -import hudson.XmlFile; -import hudson.matrix.MatrixBuild.MatrixBuildExecution; -import hudson.model.AbstractProject; -import hudson.model.Action; -import hudson.model.BuildableItemWithBuildWrappers; -import hudson.model.DependencyGraph; -import hudson.model.Descriptor; -import hudson.model.Descriptor.FormException; -import hudson.model.Node; -import hudson.slaves.WorkspaceList; -import hudson.util.AlternativeUiTextProvider; -import jenkins.model.Jenkins; -import hudson.model.Item; -import hudson.model.ItemGroup; -import hudson.model.Items; -import hudson.model.JDK; -import hudson.model.Job; -import hudson.model.Label; -import hudson.model.ParameterDefinition; -import hudson.model.ParametersDefinitionProperty; -import hudson.model.Queue.FlyweightTask; -import hudson.model.Result; -import hudson.model.SCMedItem; -import hudson.model.Saveable; -import hudson.model.TopLevelItem; -import hudson.tasks.BuildStep; -import hudson.tasks.BuildWrapper; -import hudson.tasks.BuildWrappers; -import hudson.tasks.Builder; -import hudson.tasks.Publisher; -import hudson.triggers.Trigger; -import hudson.util.CopyOnWriteMap; -import hudson.util.DescribableList; -import hudson.util.FormValidation; -import hudson.util.FormValidation.Kind; -import jenkins.scm.SCMCheckoutStrategyDescriptor; -import net.sf.json.JSONObject; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.NoExternalUse; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.TokenList; -import org.kohsuke.stapler.export.Exported; - -import javax.annotation.Nullable; -import javax.servlet.ServletException; -import java.io.File; -import java.io.FileFilter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * {@link Job} that allows you to run multiple different configurations - * from a single setting. - * - * @author Kohsuke Kawaguchi - */ -public class MatrixProject extends AbstractProject implements TopLevelItem, SCMedItem, ItemGroup, Saveable, FlyweightTask, BuildableItemWithBuildWrappers { - /** - * Configuration axes. - */ - private volatile AxisList axes = new AxisList(); - - /** - * The filter that is applied to combinations. It is a Groovy if condition. - * This can be null, which means "true". - * - * @see #getCombinationFilter() - */ - private volatile String combinationFilter; - - /** - * List of active {@link Builder}s configured for this project. - */ - private DescribableList> builders = - new DescribableList>(this); - - /** - * List of active {@link Publisher}s configured for this project. - */ - private DescribableList> publishers = - new DescribableList>(this); - - /** - * List of active {@link BuildWrapper}s configured for this project. - */ - private DescribableList> buildWrappers = - new DescribableList>(this); - - /** - * All {@link MatrixConfiguration}s, keyed by their {@link MatrixConfiguration#getName() names}. - */ - private transient /*final*/ Map configurations = new CopyOnWriteMap.Tree(); - - /** - * @see #getActiveConfigurations() - */ - @CopyOnWrite - private transient /*final*/ Set activeConfigurations = new LinkedHashSet(); - - /** - * @deprecated as of 1.456 - * Moved to {@link DefaultMatrixExecutionStrategyImpl} - */ - private transient Boolean runSequentially; - - /** - * Filter to select a number of combinations to build first - * @deprecated as of 1.456 - * Moved to {@link DefaultMatrixExecutionStrategyImpl} - */ - private transient String touchStoneCombinationFilter; - - /** - * Required result on the touchstone combinations, in order to - * continue with the rest - * @deprecated as of 1.456 - * Moved to {@link DefaultMatrixExecutionStrategyImpl} - */ - private transient Result touchStoneResultCondition; - - /** - * @deprecated as of 1.456 - * Moved to {@link DefaultMatrixExecutionStrategyImpl} - */ - private transient MatrixConfigurationSorter sorter; - - private MatrixExecutionStrategy executionStrategy; - - /** - * Custom workspace location for {@link MatrixConfiguration}s. - * - *

- * (Historically, we used {@link AbstractProject#customWorkspace} + some unique suffix (see {@link MatrixConfiguration#useShortWorkspaceName}) - * for custom workspace, but we now separated that so that the user has more control. - * - *

- * If null, the historical semantics is assumed. - * - * @since 1.466 - */ - private String childCustomWorkspace; - - public MatrixProject(String name) { - this(Jenkins.getInstance(), name); - } - - public MatrixProject(ItemGroup parent, String name) { - super(parent, name); - } - - @Override - public String getPronoun() { - return AlternativeUiTextProvider.get(PRONOUN, this, Messages.MatrixProject_Pronoun()); - } - - /** - * Gets the workspace location that {@link MatrixConfiguration} uses. - * - * @see MatrixRun.MatrixRunExecution#decideWorkspace(Node, WorkspaceList) - * - * @return never null - * even when {@link MatrixProject} uses no custom workspace, this method still - * returns something like "${PARENT_WORKSPACE}/${COMBINATION}" that controls - * how the workspace should be laid out. - * - * The return value can be absolute or relative. If relative, it is resolved - * against the working directory of the overarching {@link MatrixBuild}. - */ - public String getChildCustomWorkspace() { - String ws = childCustomWorkspace; - if (ws==null) { - ws = MatrixConfiguration.useShortWorkspaceName?"${SHORT_COMBINATION}":"${COMBINATION}"; - } - return ws; - } - - /** - * Do we have an explicit child custom workspace setting (true)? Or just using the default value (false)? - */ - public boolean hasChildCustomWorkspace() { - return childCustomWorkspace!=null; - } - - public void setChildCustomWorkspace(String childCustomWorkspace) throws IOException { - this.childCustomWorkspace = Util.fixEmptyAndTrim(childCustomWorkspace); - save(); - } - - /** - * {@link MatrixProject} is relevant with all the labels its configurations are relevant. - */ - @Override - public Set

- * By default, a {@link MatrixConfiguration} is created for every possible combination of axes exhaustively. - * But by specifying a Groovy expression as a combination filter, one can trim down the # of combinations built. - * - *

- * Namely, this expression is evaluated for each axis value combination, and only when it evaluates to true, - * a corresponding {@link MatrixConfiguration} will be created and built. - * - * @return can be null. - * @since 1.279 - */ - public String getCombinationFilter() { - return combinationFilter; - } - - /** - * @return can be null (to indicate that the configurations should be left to their natural order.) - * @deprecated as of 1.456 - * Use {@link DefaultMatrixExecutionStrategyImpl#getTouchStoneCombinationFilter()}. - * This method tries to emulate the previous behavior the best it can, but will return null - * if the current {@link MatrixExecutionStrategy} is not the default one. - */ - public String getTouchStoneCombinationFilter() { - MatrixExecutionStrategy e = executionStrategy; - if (e instanceof DefaultMatrixExecutionStrategyImpl) { - DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e; - return dm.getTouchStoneCombinationFilter(); - } - return null; - } - - /** - * @deprecated as of 1.456 - * Use {@link DefaultMatrixExecutionStrategyImpl#setTouchStoneCombinationFilter(String)}. - * This method tries to emulate the previous behavior the best it can, but will fall back - * to no-op if the current {@link MatrixExecutionStrategy} is not the default one. - */ - public void setTouchStoneCombinationFilter(String touchStoneCombinationFilter) throws IOException { - MatrixExecutionStrategy e = executionStrategy; - if (e instanceof DefaultMatrixExecutionStrategyImpl) { - DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e; - dm.setTouchStoneCombinationFilter(touchStoneCombinationFilter); - save(); - } - } - - /** - * @return can be null (to indicate that the configurations should be left to their natural order.) - * @deprecated as of 1.456 - * Use {@link DefaultMatrixExecutionStrategyImpl#getTouchStoneResultCondition()}. - * This method tries to emulate the previous behavior the best it can, but will return null - * if the current {@link MatrixExecutionStrategy} is not the default one. - */ - public Result getTouchStoneResultCondition() { - MatrixExecutionStrategy e = executionStrategy; - if (e instanceof DefaultMatrixExecutionStrategyImpl) { - DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e; - return dm.getTouchStoneResultCondition(); - } - return null; - } - - /** - * @deprecated as of 1.456 - * Use {@link DefaultMatrixExecutionStrategyImpl#setTouchStoneResultCondition(Result)}. - * This method tries to emulate the previous behavior the best it can, but will fall back - * to no-op if the current {@link MatrixExecutionStrategy} is not the default one. - */ - public void setTouchStoneResultCondition(Result touchStoneResultCondition) throws IOException { - MatrixExecutionStrategy e = executionStrategy; - if (e instanceof DefaultMatrixExecutionStrategyImpl) { - DefaultMatrixExecutionStrategyImpl dm = (DefaultMatrixExecutionStrategyImpl) e; - dm.setTouchStoneResultCondition(touchStoneResultCondition); - save(); - } - } - - @Override - protected List createTransientActions() { - List r = super.createTransientActions(); - - for (BuildStep step : builders) - r.addAll(step.getProjectActions(this)); - for (BuildStep step : publishers) - r.addAll(step.getProjectActions(this)); - for (BuildWrapper step : buildWrappers) - r.addAll(step.getProjectActions(this)); - for (Trigger trigger : triggers()) - r.addAll(trigger.getProjectActions()); - - return r; - } - - @Override - protected void updateTransientActions(){ - super.updateTransientActions(); - if(getActiveConfigurations() !=null){ - // update all transient actions in configurations too. - for(MatrixConfiguration configuration: getActiveConfigurations()){ - configuration.updateTransientActions(); - } - } - } - - /** - * Gets the subset of {@link AxisList} that are not system axes. - * - * @deprecated as of 1.373 - * System vs user difference are generalized into extension point. - */ - public List getUserAxes() { - List r = new ArrayList(); - for (Axis a : axes) - if(!a.isSystem()) - r.add(a); - return r; - } - - public Layouter getLayouter() { - return new Layouter(axes) { - protected MatrixConfiguration getT(Combination c) { - return getItem(c); - } - }; - } - - @Override - public void onCreatedFromScratch() { - executionStrategy = new DefaultMatrixExecutionStrategyImpl(); - super.onCreatedFromScratch(); - } - - @Override - public void onLoad(ItemGroup parent, String name) throws IOException { - super.onLoad(parent,name); - builders.setOwner(this); - publishers.setOwner(this); - buildWrappers.setOwner(this); - - if (executionStrategy ==null) - executionStrategy = new DefaultMatrixExecutionStrategyImpl(runSequentially != null ? runSequentially : false, touchStoneCombinationFilter, touchStoneResultCondition, sorter); - - rebuildConfigurations(null); - } - - @Override - public void logRotate() throws IOException, InterruptedException { - super.logRotate(); - // perform the log rotation of inactive configurations to make sure - // their logs get eventually discarded - for (MatrixConfiguration config : configurations.values()) { - if(!config.isActiveConfiguration()) - config.logRotate(); - } - } - - /** - * Recursively search for configuration and put them to the map - * - *

- * The directory structure would be axis-a/b/axis-c/d/axis-e/f for - * combination [a=b,c=d,e=f]. Note that two combinations [a=b,c=d] and [a=b,c=d,e=f] - * can both co-exist (where one is an archived record and the other is live, for example) - * so search needs to be thorough. - * - * @param dir - * Directory to be searched. - * @param result - * Receives the loaded {@link MatrixConfiguration}s. - * @param combination - * Combination of key/values discovered so far while traversing the directories. - * Read-only. - */ - private void loadConfigurations( File dir, CopyOnWriteMap.Tree result, Map combination ) { - File[] axisDirs = dir.listFiles(new FileFilter() { - public boolean accept(File child) { - return child.isDirectory() && child.getName().startsWith("axis-"); - } - }); - if(axisDirs==null) return; - - for (File subdir : axisDirs) { - String axis = subdir.getName().substring(5); // axis name - - File[] valuesDir = subdir.listFiles(new FileFilter() { - public boolean accept(File child) { - return child.isDirectory(); - } - }); - if(valuesDir==null) continue; // no values here - - for (File v : valuesDir) { - Map c = new HashMap(combination); - c.put(axis,TokenList.decode(v.getName())); - - try { - XmlFile config = Items.getConfigFile(v); - if(config.exists()) { - Combination comb = new Combination(c); - // if we already have this in memory, just use it. - // otherwise load it - MatrixConfiguration item=null; - if(this.configurations!=null) - item = this.configurations.get(comb); - if(item==null) { - item = (MatrixConfiguration) config.read(); - item.setCombination(comb); - item.onLoad(this, v.getName()); - } - result.put(item.getCombination(), item); - } - } catch (IOException e) { - LOGGER.log(Level.WARNING, "Failed to load matrix configuration "+v,e); - } - loadConfigurations(v,result,c); - } - } - } - - /** - * Rebuilds the {@link #configurations} list and {@link #activeConfigurations}. - * - * @param context - * We rebuild configurations right before a build, to allow configurations to be adjusted for the build. - * (think of it as reconfiguring a project right before a build.) And when that happens, this value is the - * build in progress. Otherwise this value is null (for example, when Jenkins is booting up.) - */ - /*package*/ Set rebuildConfigurations(MatrixBuildExecution context) throws IOException { - { - // backward compatibility check to see if there's any data in the old structure - // if so, bring them to the newer structure. - File[] oldDirs = getConfigurationsDir().listFiles(new FileFilter() { - public boolean accept(File child) { - return child.isDirectory() && !child.getName().startsWith("axis-"); - } - }); - if(oldDirs!=null) { - // rename the old directory to the new one - for (File dir : oldDirs) { - try { - Combination c = Combination.fromString(dir.getName()); - dir.renameTo(getRootDirFor(c)); - } catch (IllegalArgumentException e) { - // it's not a configuration dir. Just ignore. - } - } - } - } - - CopyOnWriteMap.Tree configurations = - new CopyOnWriteMap.Tree(); - loadConfigurations(getConfigurationsDir(),configurations,Collections.emptyMap()); - this.configurations = configurations; - - Iterable activeCombinations; - if (context!=null) { - List> axesList = Lists.newArrayList(); - for (Axis axis : axes) - axesList.add(Sets.newLinkedHashSet(axis.rebuild(context))); - - activeCombinations = Iterables.transform(Sets.cartesianProduct(axesList), new Function, Combination>() { - public Combination apply(@Nullable List strings) { - assert strings != null; - return new Combination(axes, (String[]) strings.toArray(new String[0])); - } - }); - } else { - activeCombinations = axes.list(); - } - - // find all active configurations - final Set active = new LinkedHashSet(); - final boolean isDynamicFilter = isDynamicFilter(getCombinationFilter()); - - for (Combination c : activeCombinations) { - if(isDynamicFilter || c.evalGroovyExpression(axes,getCombinationFilter())) { - LOGGER.fine("Adding configuration: " + c); - MatrixConfiguration config = configurations.get(c); - if(config==null) { - config = new MatrixConfiguration(this,c); - config.onCreatedFromScratch(); - config.save(); - configurations.put(config.getCombination(), config); - } - active.add(config); - } - } - this.activeConfigurations = active; - - return active; - } - - private boolean isDynamicFilter(final String filter) { - - if (!isParameterized() || filter == null) return false; - - final ParametersDefinitionProperty paramDefProp = getProperty(ParametersDefinitionProperty.class); - - for (final ParameterDefinition definition : paramDefProp.getParameterDefinitions()) { - - final String name = definition.getName(); - - final Matcher matcher = Pattern - .compile("\\b" + name + "\\b") - .matcher(filter) - ; - - if (matcher.find()) return true; - } - - return false; - } - - private File getConfigurationsDir() { - return new File(getRootDir(),"configurations"); - } - - /** - * Gets all active configurations. - * - *

- * In contract, inactive configurations are those that are left for archival purpose - * and no longer built when a new {@link MatrixBuild} is executed. - * - *

- * During a build, {@link MatrixBuildExecution#getActiveConfigurations()} should be used - * to make sure that a build is using the consistent set of active configurations from - * the start to the end. - */ - @Exported - public Collection getActiveConfigurations() { - return activeConfigurations; - } - - public Collection getItems() { - return configurations.values(); - } - - @Override - public Collection getAllJobs() { - Set jobs = new HashSet(getItems()); - jobs.add(this); - return jobs; - } - - public String getUrlChildPrefix() { - return "."; - } - - public MatrixConfiguration getItem(String name) { - try { - return getItem(Combination.fromString(name)); - } catch (IllegalArgumentException e) { - return null; - } - } - - public MatrixConfiguration getItem(Combination c) { - if (configurations == null) { - return null; - } - return configurations.get(c); - } - - public File getRootDirFor(MatrixConfiguration child) { - return getRootDirFor(child.getCombination()); - } - - public void onRenamed(MatrixConfiguration item, String oldName, String newName) throws IOException { - throw new UnsupportedOperationException(); - } - - public void onDeleted(MatrixConfiguration item) throws IOException { - // noop - } - - public File getRootDirFor(Combination combination) { - File f = getConfigurationsDir(); - for (Entry e : combination.entrySet()) - f = new File(f,"axis-"+e.getKey()+'/'+Util.rawEncode(e.getValue())); - f.getParentFile().mkdirs(); - return f; - } - - /** - * @see #getJDKs() - */ - @Override @Deprecated - public JDK getJDK() { - return super.getJDK(); - } - - /** - * Gets the {@link JDK}s where the builds will be run. - * @return never null but can be empty - */ - public Set getJDKs() { - Axis a = axes.find("jdk"); - if(a==null) return Collections.emptySet(); - Set r = new HashSet(); - for (String j : a) { - JDK jdk = Jenkins.getInstance().getJDK(j); - if(jdk!=null) - r.add(jdk); - } - return r; - } - - /** - * Gets the {@link Label}s where the builds will be run. - * @return never null - */ - public Set

- * Plugins can implement this extension point to filter out the subset of matrix project to build. - * Most typically, such a plugin would add a custom {@link Action} to a build when it goes to the queue - * ({@link Queue#schedule2(Task, int, List)}, then access this from {@link MatrixBuild} to drive - * the filtering logic. - * - *

- * See the matrix reloaded plugin for an example. - * - * @author Christian Wolfgang - * @since 1.413 - */ -public abstract class MatrixBuildListener implements ExtensionPoint { - /** - * Determine whether to build a given configuration or not - * - * @param b - * Never null. The umbrella build. - * @param c - * The configuration whose build is being considered. If any of the {@link MatrixBuildListener} - * returns false, then the build for this configuration is skipped, and the previous build - * of this configuration will be taken as the default result. - * @return - * True to let the build happen, false to skip it. - */ - public abstract boolean doBuildConfiguration(MatrixBuild b, MatrixConfiguration c); - - public static boolean buildConfiguration(MatrixBuild b, MatrixConfiguration c) { - for (MatrixBuildListener l : all()) { - if(!l.doBuildConfiguration(b, c)) { - return false; - } - } - - return true; - } - - /** - * Returns all the registered {@link MatrixBuildListener} descriptors. - */ - public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(MatrixBuildListener.class); - } -} diff --git a/core/src/main/java/hudson/matrix/package.html b/core/src/main/java/hudson/matrix/package.html deleted file mode 100644 index 72e14d2dcbbce235432716c330f103d013d2ad29..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/package.html +++ /dev/null @@ -1,27 +0,0 @@ - - - -Matrix project - \ No newline at end of file diff --git a/core/src/main/java/hudson/model/AbstractBuild.java b/core/src/main/java/hudson/model/AbstractBuild.java index 256c59b7d732f8a3573c1fcc5d71799c59053121..797d2dae2baddad1e3d46961e663c66282b6b608 100644 --- a/core/src/main/java/hudson/model/AbstractBuild.java +++ b/core/src/main/java/hudson/model/AbstractBuild.java @@ -30,13 +30,12 @@ import hudson.EnvVars; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; -import hudson.Util; import hudson.console.AnnotatedLargeText; import hudson.console.ExpandableDetailsNote; import hudson.console.ModelHyperlinkNote; -import hudson.matrix.MatrixConfiguration; import hudson.model.Fingerprint.BuildPtr; import hudson.model.Fingerprint.RangeSet; +import hudson.model.labels.LabelAtom; import hudson.model.listeners.RunListener; import hudson.model.listeners.SCMListener; import hudson.scm.ChangeLogParser; @@ -44,6 +43,7 @@ import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import hudson.scm.NullChangeLogParser; import hudson.scm.SCM; +import hudson.scm.SCMRevisionState; import hudson.slaves.NodeProperty; import hudson.slaves.WorkspaceList; import hudson.slaves.WorkspaceList.Lease; @@ -58,8 +58,6 @@ import hudson.tasks.test.AbstractTestResultAction; import hudson.tasks.test.AggregatedTestResultAction; import hudson.util.*; import jenkins.model.Jenkins; -import jenkins.model.lazy.AbstractLazyLoadRunMap.Direction; -import jenkins.model.lazy.BuildReference; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; @@ -73,7 +71,6 @@ import java.io.IOException; import java.io.InterruptedIOException; import java.io.StringWriter; import java.lang.ref.WeakReference; -import java.text.MessageFormat; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Calendar; @@ -88,10 +85,16 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import org.kohsuke.stapler.interceptor.RequirePOST; import static java.util.logging.Level.WARNING; +import jenkins.model.lazy.BuildReference; +import jenkins.model.lazy.LazyBuildMixIn; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; + /** * Base implementation of {@link Run}s that build software. * @@ -100,7 +103,7 @@ import static java.util.logging.Level.WARNING; * @author Kohsuke Kawaguchi * @see AbstractProject */ -public abstract class AbstractBuild

,R extends AbstractBuild> extends Run implements Queue.Executable { +public abstract class AbstractBuild

,R extends AbstractBuild> extends Run implements Queue.Executable, LazyBuildMixIn.LazyLoadingRun { /** * Set if we want the blame information to flow from upstream to downstream build. @@ -156,19 +159,11 @@ public abstract class AbstractBuild

,R extends Abs */ protected transient List buildEnvironments; - /** - * Pointers to form bi-directional link between adjacent {@link AbstractBuild}s. - * - *

- * Unlike {@link Run}, {@link AbstractBuild}s do lazy-loading, so we don't use - * {@link Run#previousBuild} and {@link Run#nextBuild}, and instead use these - * fields and point to {@link #selfReference} of adjacent builds. - */ - private volatile transient BuildReference previousBuild, nextBuild; - - /*package*/ final transient BuildReference selfReference = new BuildReference(getId(),_this()); - - + private transient final LazyBuildMixIn.RunMixIn runMixIn = new LazyBuildMixIn.RunMixIn() { + @Override protected R asRun() { + return _this(); + } + }; protected AbstractBuild(P job) throws IOException { super(job); @@ -186,88 +181,26 @@ public abstract class AbstractBuild

,R extends Abs return getParent(); } - @Override - void dropLinks() { - super.dropLinks(); - - if(nextBuild!=null) { - AbstractBuild nb = nextBuild.get(); - if (nb!=null) { - // remove the oldest build - if (previousBuild == selfReference) - nb.previousBuild = nextBuild; - else - nb.previousBuild = previousBuild; - } - } - if(previousBuild!=null) { - AbstractBuild pb = previousBuild.get(); - if (pb!=null) pb.nextBuild = nextBuild; - } + @Override public final LazyBuildMixIn.RunMixIn getRunMixIn() { + return runMixIn; } - @Override - public R getPreviousBuild() { - while (true) { - BuildReference r = previousBuild; // capture the value once - - if (r==null) { - // having two neighbors pointing to each other is important to make RunMap.removeValue work - P _parent = getParent(); - if (_parent == null) { - throw new IllegalStateException("no parent for " + number + " in " + workspace); - } - R pb = _parent._getRuns().search(number-1, Direction.DESC); - if (pb!=null) { - ((AbstractBuild)pb).nextBuild = selfReference; // establish bi-di link - this.previousBuild = pb.selfReference; - return pb; - } else { - // this indicates that we know there's no previous build - // (as opposed to we don't know if/what our previous build is. - this.previousBuild = selfReference; - return null; - } - } - if (r==selfReference) - return null; + @Override protected final BuildReference createReference() { + return getRunMixIn().createReference(); + } - R referent = r.get(); - if (referent!=null) return referent; + @Override protected final void dropLinks() { + getRunMixIn().dropLinks(); + } - // the reference points to a GC-ed object, drop the reference and do it again - this.previousBuild = null; - } + @Override + public R getPreviousBuild() { + return getRunMixIn().getPreviousBuild(); } @Override public R getNextBuild() { - while (true) { - BuildReference r = nextBuild; // capture the value once - - if (r==null) { - // having two neighbors pointing to each other is important to make RunMap.removeValue work - R nb = getParent().builds.search(number+1, Direction.ASC); - if (nb!=null) { - ((AbstractBuild)nb).previousBuild = selfReference; // establish bi-di link - this.nextBuild = nb.selfReference; - return nb; - } else { - // this indicates that we know there's no next build - // (as opposed to we don't know if/what our next build is. - this.nextBuild = selfReference; - return null; - } - } - if (r==selfReference) - return null; - - R referent = r.get(); - if (referent!=null) return referent; - - // the reference points to a GC-ed object, drop the reference and do it again - this.nextBuild = null; - } + return getRunMixIn().getNextBuild(); } /** @@ -421,8 +354,8 @@ public abstract class AbstractBuild

,R extends Abs if (upstreamCulprits) { // If we have dependencies since the last successful build, add their authors to our list if (getPreviousNotFailedBuild() != null) { - Map depmap = getDependencyChanges(getPreviousSuccessfulBuild()); - for (AbstractBuild.DependencyChange dep : depmap.values()) { + Map depmap = getDependencyChanges(getPreviousSuccessfulBuild()); + for (DependencyChange dep : depmap.values()) { for (AbstractBuild b : dep.getBuilds()) { for (Entry entry : b.getChangeSet()) { r.add(entry.getAuthor()); @@ -500,11 +433,27 @@ public abstract class AbstractBuild

,R extends Abs */ protected BuildListener listener; + /** + * Lease of the workspace. + */ + private Lease lease; + /** * Returns the current {@link Node} on which we are building. + * @return Returns the current {@link Node} + * @throws IllegalStateException if that cannot be determined */ - protected final Node getCurrentNode() { - return Executor.currentExecutor().getOwner().getNode(); + protected final @Nonnull Node getCurrentNode() throws IllegalStateException { + Executor exec = Executor.currentExecutor(); + if (exec == null) { + throw new IllegalStateException("not being called from an executor thread"); + } + Computer c = exec.getOwner(); + Node node = c.getNode(); + if (node == null) { + throw new IllegalStateException("no longer a configured node for " + c.getName()); + } + return node; } public Launcher getLauncher() { @@ -523,7 +472,7 @@ public abstract class AbstractBuild

,R extends Abs * @param wsl * Passed in for the convenience. The returned path must be registered to this object. */ - protected Lease decideWorkspace(Node n, WorkspaceList wsl) throws InterruptedException, IOException { + protected Lease decideWorkspace(@Nonnull Node n, WorkspaceList wsl) throws InterruptedException, IOException { String customWorkspace = getProject().getCustomWorkspace(); if (customWorkspace != null) { // we allow custom workspaces to be concurrently used between jobs. @@ -533,72 +482,87 @@ public abstract class AbstractBuild

,R extends Abs return wsl.allocate(n.getWorkspaceFor((TopLevelItem)getProject()), getBuild()); } - public Result run(BuildListener listener) throws Exception { - Node node = getCurrentNode(); + public Result run(@Nonnull BuildListener listener) throws Exception { + final Node node = getCurrentNode(); + assert builtOn==null; builtOn = node.getNodeName(); hudsonVersion = Jenkins.VERSION; this.listener = listener; launcher = createLauncher(listener); - if (!Jenkins.getInstance().getNodes().isEmpty()) - listener.getLogger().print(node instanceof Jenkins ? Messages.AbstractBuild_BuildingOnMaster() : - Messages.AbstractBuild_BuildingRemotely(ModelHyperlinkNote.encodeTo("/computer/" + builtOn, builtOn))); - else - listener.getLogger().print(Messages.AbstractBuild_Building()); + if (!Jenkins.getInstance().getNodes().isEmpty()) { + if (node instanceof Jenkins) { + listener.getLogger().print(Messages.AbstractBuild_BuildingOnMaster()); + } else { + listener.getLogger().print(Messages.AbstractBuild_BuildingRemotely(ModelHyperlinkNote.encodeTo("/computer/" + builtOn, builtOn))); + Set assignedLabels = new HashSet(node.getAssignedLabels()); + assignedLabels.remove(node.getSelfLabel()); + if (!assignedLabels.isEmpty()) { + boolean first = true; + for (LabelAtom label : assignedLabels) { + if (first) { + listener.getLogger().print(" ("); + first = false; + } else { + listener.getLogger().print(' '); + } + listener.getLogger().print(label.getName()); + } + listener.getLogger().print(')'); + } + } + } else { + listener.getLogger().print(Messages.AbstractBuild_Building()); + } - final Lease lease = decideWorkspace(node,Computer.currentComputer().getWorkspaceList()); + lease = decideWorkspace(node, Computer.currentComputer().getWorkspaceList()); - try { - workspace = lease.path.getRemote(); - listener.getLogger().println(Messages.AbstractBuild_BuildingInWorkspace(workspace)); - node.getFileSystemProvisioner().prepareWorkspace(AbstractBuild.this,lease.path,listener); - - for (WorkspaceListener wl : WorkspaceListener.all()) { - wl.beforeUse(AbstractBuild.this, lease.path, listener); - } + workspace = lease.path.getRemote(); + listener.getLogger().println(Messages.AbstractBuild_BuildingInWorkspace(workspace)); + node.getFileSystemProvisioner().prepareWorkspace(AbstractBuild.this,lease.path,listener); + + for (WorkspaceListener wl : WorkspaceListener.all()) { + wl.beforeUse(AbstractBuild.this, lease.path, listener); + } - getProject().getScmCheckoutStrategy().preCheckout(AbstractBuild.this, launcher, this.listener); - getProject().getScmCheckoutStrategy().checkout(this); + getProject().getScmCheckoutStrategy().preCheckout(AbstractBuild.this, launcher, this.listener); + getProject().getScmCheckoutStrategy().checkout(this); - if (!preBuild(listener,project.getProperties())) - return Result.FAILURE; + if (!preBuild(listener,project.getProperties())) + return Result.FAILURE; - Result result = doRun(listener); + Result result = doRun(listener); - Computer c = node.toComputer(); - if (c==null || c.isOffline()) { - // As can be seen in HUDSON-5073, when a build fails because of the slave connectivity problem, - // error message doesn't point users to the slave. So let's do it here. - listener.hyperlink("/computer/"+builtOn+"/log","Looks like the node went offline during the build. Check the slave log for the details."); + Computer c = node.toComputer(); + if (c==null || c.isOffline()) { + // As can be seen in HUDSON-5073, when a build fails because of the slave connectivity problem, + // error message doesn't point users to the slave. So let's do it here. + listener.hyperlink("/computer/"+builtOn+"/log","Looks like the node went offline during the build. Check the slave log for the details."); - if (c != null) { - // grab the end of the log file. This might not work very well if the slave already - // starts reconnecting. Fixing this requires a ring buffer in slave logs. - AnnotatedLargeText log = c.getLogText(); - StringWriter w = new StringWriter(); - log.writeHtmlTo(Math.max(0,c.getLogFile().length()-10240),w); + if (c != null) { + // grab the end of the log file. This might not work very well if the slave already + // starts reconnecting. Fixing this requires a ring buffer in slave logs. + AnnotatedLargeText log = c.getLogText(); + StringWriter w = new StringWriter(); + log.writeHtmlTo(Math.max(0,c.getLogFile().length()-10240),w); - listener.getLogger().print(ExpandableDetailsNote.encodeTo("details",w.toString())); - listener.getLogger().println(); - } + listener.getLogger().print(ExpandableDetailsNote.encodeTo("details",w.toString())); + listener.getLogger().println(); } + } - // kill run-away processes that are left - // use multiple environment variables so that people can escape this massacre by overriding an environment - // variable for some processes - launcher.kill(getCharacteristicEnvVars()); + // kill run-away processes that are left + // use multiple environment variables so that people can escape this massacre by overriding an environment + // variable for some processes + launcher.kill(getCharacteristicEnvVars()); - // this is ugly, but for historical reason, if non-null value is returned - // it should become the final result. - if (result==null) result = getResult(); - if (result==null) result = Result.SUCCESS; + // this is ugly, but for historical reason, if non-null value is returned + // it should become the final result. + if (result==null) result = getResult(); + if (result==null) result = Result.SUCCESS; - return result; - } finally { - lease.release(); - this.listener = null; - } + return result; } /** @@ -608,8 +572,10 @@ public abstract class AbstractBuild

,R extends Abs * @param listener * Always non-null. Connected to the main build output. */ - protected Launcher createLauncher(BuildListener listener) throws IOException, InterruptedException { - Launcher l = getCurrentNode().createLauncher(listener); + @Nonnull + protected Launcher createLauncher(@Nonnull BuildListener listener) throws IOException, InterruptedException { + final Node currentNode = getCurrentNode(); + Launcher l = currentNode.createLauncher(listener); if (project instanceof BuildableItemWithBuildWrappers) { BuildableItemWithBuildWrappers biwbw = (BuildableItemWithBuildWrappers) project; @@ -633,7 +599,7 @@ public abstract class AbstractBuild

,R extends Abs } } - for (NodeProperty nodeProperty: Computer.currentComputer().getNode().getNodeProperties()) { + for (NodeProperty nodeProperty: currentNode.getNodeProperties()) { Environment environment = nodeProperty.setUp(AbstractBuild.this, l, listener); if (environment != null) { buildEnvironments.add(environment); @@ -654,18 +620,26 @@ public abstract class AbstractBuild

,R extends Abs build.scm = NullChangeLogParser.INSTANCE; try { - if (project.checkout(build,launcher,listener,new File(build.getRootDir(),"changelog.xml"))) { + File changeLogFile = new File(build.getRootDir(), "changelog.xml"); + if (project.checkout(build, launcher,listener, changeLogFile)) { // check out succeeded SCM scm = project.getScm(); + for (SCMListener l : SCMListener.all()) { + try { + l.onCheckout(build, scm, build.getWorkspace(), listener, changeLogFile, build.getAction(SCMRevisionState.class)); + } catch (Exception e) { + throw new IOException(e); + } + } build.scm = scm.createChangeLogParser(); build.changeSet = new WeakReference>(build.calcChangeSet()); - for (SCMListener l : Jenkins.getInstance().getSCMListeners()) + for (SCMListener l : SCMListener.all()) try { l.onChangeLogParsed(build,listener,build.getChangeSet()); } catch (Exception e) { - throw new IOException2("Failed to parse changelog",e); + throw new IOException("Failed to parse changelog",e); } // Get a chance to do something after checkout and changelog is done @@ -720,6 +694,10 @@ public abstract class AbstractBuild

,R extends Abs } public void cleanUp(BuildListener listener) throws Exception { + if (lease!=null) { + lease.release(); + lease = null; + } BuildTrigger.execute(AbstractBuild.this, listener); buildEnvironments = null; } @@ -756,19 +734,27 @@ public abstract class AbstractBuild

,R extends Abs if ((bs instanceof Publisher && ((Publisher)bs).needsToRunAfterFinalized()) ^ phase) try { if (!perform(bs,listener)) { - LOGGER.fine(MessageFormat.format("{0} : {1} failed", AbstractBuild.this.toString(), bs)); + LOGGER.log(Level.FINE, "{0} : {1} failed", new Object[] {AbstractBuild.this, bs}); r = false; } } catch (Exception e) { - String msg = "Publisher " + bs.getClass().getName() + " aborted due to exception"; - e.printStackTrace(listener.error(msg)); - LOGGER.log(WARNING, msg, e); - setResult(Result.FAILURE); + reportError(bs, e, listener, phase); + } catch (LinkageError e) { + reportError(bs, e, listener, phase); } } return r; } + private void reportError(BuildStep bs, Throwable e, BuildListener listener, boolean phase) { + String msg = "Publisher " + bs.getClass().getName() + " aborted due to exception"; + e.printStackTrace(listener.error(msg)); + LOGGER.log(WARNING, msg, e); + if (phase) { + setResult(Result.FAILURE); + } + } + /** * Calls a build step. */ @@ -818,7 +804,7 @@ public abstract class AbstractBuild

,R extends Abs protected final boolean preBuild(BuildListener listener,Iterable steps) { for (BuildStep bs : steps) if (!bs.prebuild(AbstractBuild.this,listener)) { - LOGGER.fine(MessageFormat.format("{0} : {1} failed", AbstractBuild.this.toString(), bs)); + LOGGER.log(Level.FINE, "{0} : {1} failed", new Object[] {AbstractBuild.this, bs}); return false; } return true; @@ -887,6 +873,12 @@ public abstract class AbstractBuild

,R extends Abs return cs; } + @Restricted(DoNotUse.class) // for project-changes.jelly + public List> getChangeSets() { + ChangeLogSet cs = getChangeSet(); + return cs.isEmptySet() ? Collections.>emptyList() : Collections.>singletonList(cs); + } + /** * Returns true if the changelog is already computed. */ @@ -923,7 +915,7 @@ public abstract class AbstractBuild

,R extends Abs for (Environment e : buildEnvironments) e.buildEnvVars(env); - for (EnvironmentContributingAction a : Util.filter(getActions(),EnvironmentContributingAction.class)) + for (EnvironmentContributingAction a : getActions(EnvironmentContributingAction.class)) a.buildEnvVars(this,env); EnvVars.resolve(env); @@ -956,7 +948,16 @@ public abstract class AbstractBuild

,R extends Abs public Calendar due() { return getTimestamp(); } + + /** + * {@inheritDoc} + * The action may have a {@code summary.jelly} view containing a {@code } or other {@code }. + */ + @Override public void addAction(Action a) { + super.addAction(a); + } + @SuppressWarnings("deprecation") public List getPersistentActions(){ return super.getActions(); } @@ -995,7 +996,7 @@ public abstract class AbstractBuild

,R extends Abs * Provides additional variables and their values to {@link Builder}s. * *

- * This mechanism is used by {@link MatrixConfiguration} to pass + * This mechanism is used by {@code MatrixConfiguration} to pass * the configuration values to the current build. It is up to * {@link Builder}s to decide whether they want to recognize the values * or how to use them. diff --git a/core/src/main/java/hudson/model/AbstractCIBase.java b/core/src/main/java/hudson/model/AbstractCIBase.java index 69328fa4c963a1eb2ac675021516f047b5c8f947..08254d78082ea6b9cf0e4487bafdfd4d4b297a90 100644 --- a/core/src/main/java/hudson/model/AbstractCIBase.java +++ b/core/src/main/java/hudson/model/AbstractCIBase.java @@ -54,6 +54,7 @@ public abstract class AbstractCIBase extends Node implements ItemGroup byName = new HashMap(); for (Computer c : computers.values()) { - if(c.getNode()==null) + Node node = c.getNode(); + if (node == null) continue; // this computer is gone - byName.put(c.getNode().getNodeName(),c); + byName.put(node.getNodeName(),c); } Set old = new HashSet(computers.values()); diff --git a/core/src/main/java/hudson/model/AbstractDescribableImpl.java b/core/src/main/java/hudson/model/AbstractDescribableImpl.java index 50df6771ba6caa38fef2a39b8de52d36a53b4e4b..5961a80834a13feb490f3d29fd3976860913dcda 100644 --- a/core/src/main/java/hudson/model/AbstractDescribableImpl.java +++ b/core/src/main/java/hudson/model/AbstractDescribableImpl.java @@ -1,37 +1,44 @@ -/* - * The MIT License - * - * Copyright (c) 2004-2009, Kohsuke Kawaguchi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package hudson.model; - -import jenkins.model.Jenkins; - -/** - * Partial default implementation of {@link Describable}. - * - * @author Kohsuke Kawaguchi - */ -public abstract class AbstractDescribableImpl> implements Describable { - public Descriptor getDescriptor() { - return Jenkins.getInstance().getDescriptorOrDie(getClass()); - } -} +/* + * The MIT License + * + * Copyright (c) 2004-2009, Kohsuke Kawaguchi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.model; + +import hudson.Extension; +import jenkins.model.Jenkins; + +/** + * Partial default implementation of {@link Describable}. + * + * @author Kohsuke Kawaguchi + */ +public abstract class AbstractDescribableImpl> implements Describable { + + /** + * By default looks for a nested class (conventionally named {@code DescriptorImpl}) implementing {@link Descriptor} and marked with {@link Extension}. + *

{@inheritDoc} + */ + public Descriptor getDescriptor() { + return Jenkins.getInstance().getDescriptorOrDie(getClass()); + } + +} diff --git a/core/src/main/java/hudson/model/AbstractItem.java b/core/src/main/java/hudson/model/AbstractItem.java index 6419fc2b456013c87747af0df51fcf78575d8e97..115a01e63597ffbd3b9236f39732e71b386bf2c1 100644 --- a/core/src/main/java/hudson/model/AbstractItem.java +++ b/core/src/main/java/hudson/model/AbstractItem.java @@ -34,13 +34,13 @@ import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SaveableListener; +import hudson.remoting.Callable; import hudson.security.AccessControlled; import hudson.security.Permission; import hudson.security.ACL; import hudson.util.AlternativeUiTextProvider; import hudson.util.AlternativeUiTextProvider.Message; import hudson.util.AtomicFileWriter; -import hudson.util.IOException2; import hudson.util.IOUtils; import jenkins.model.Jenkins; import org.apache.tools.ant.taskdefs.Copy; @@ -52,6 +52,11 @@ import org.kohsuke.stapler.export.ExportedBean; import java.io.File; import java.io.IOException; import java.util.Collection; +import java.util.List; +import java.util.ListIterator; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nonnull; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; @@ -70,6 +75,7 @@ import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import org.kohsuke.stapler.Ancestor; /** * Partial default implementation of {@link Item}. @@ -80,6 +86,9 @@ import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; // Java doesn't let multiple inheritance. @ExportedBean public abstract class AbstractItem extends Actionable implements Item, HttpDeletable, AccessControlled, DescriptorByNameOwner { + + private static final Logger LOGGER = Logger.getLogger(AbstractItem.class.getName()); + /** * Project name. */ @@ -157,15 +166,17 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet } public File getRootDir() { - return parent.getRootDirFor(this); + return getParent().getRootDirFor(this); } /** * This bridge method is to maintain binary compatibility with {@link TopLevelItem#getParent()}. */ @WithBridgeMethods(value=Jenkins.class,castRequired=true) - public ItemGroup getParent() { - assert parent!=null; + @Override public @Nonnull ItemGroup getParent() { + if (parent == null) { + throw new IllegalStateException("no parent set on " + getClass().getName() + "[" + name + "]"); + } return parent; } @@ -221,6 +232,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet + " already exists"); String oldName = this.name; + String oldFullName = getFullName(); File oldRoot = this.getRootDir(); doSetName(newName); @@ -265,7 +277,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet cp.setProject(new org.apache.tools.ant.Project()); cp.setTodir(newRoot); FileSet src = new FileSet(); - src.setDir(getRootDir()); + src.setDir(oldRoot); cp.addFileset(src); cp.setOverwrite(true); cp.setPreserveLastModified(true); @@ -291,8 +303,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet callOnRenamed(newName, parent, oldName); - for (ItemListener l : ItemListener.all()) - l.onRenamed(this, oldName, newName); + ItemListener.fireLocationChange(this, oldFullName); } } } @@ -323,7 +334,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet public final String getFullDisplayName() { String n = getParent().getFullDisplayName(); if(n.length()==0) return getDisplayName(); - else return n+" \u00BB "+getDisplayName(); + else return n+" » "+getDisplayName(); } /** @@ -364,7 +375,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet /** * Called right after when a {@link Item} is loaded from disk. - * This is an opporunity to do a post load processing. + * This is an opportunity to do a post load processing. */ public void onLoad(ItemGroup parent, String name) throws IOException { this.parent = parent; @@ -389,16 +400,41 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet public final String getUrl() { // try to stick to the current view if possible StaplerRequest req = Stapler.getCurrentRequest(); + String shortUrl = getShortUrl(); + String uri = req == null ? null : req.getRequestURI(); if (req != null) { String seed = Functions.getNearestAncestorUrl(req,this); + LOGGER.log(Level.FINER, "seed={0} for {1} from {2}", new Object[] {seed, this, uri}); if(seed!=null) { // trim off the context path portion and leading '/', but add trailing '/' return seed.substring(req.getContextPath().length()+1)+'/'; } + List ancestors = req.getAncestors(); + if (!ancestors.isEmpty()) { + Ancestor last = ancestors.get(ancestors.size() - 1); + if (last.getObject() instanceof View) { + View view = (View) last.getObject(); + if (view.getOwnerItemGroup() == getParent() && !view.isDefault()) { + // Showing something inside a view, so should use that as the base URL. + String base = last.getUrl().substring(req.getContextPath().length() + 1) + '/'; + LOGGER.log(Level.FINER, "using {0}{1} for {2} from {3}", new Object[] {base, shortUrl, this, uri}); + return base + shortUrl; + } else { + LOGGER.log(Level.FINER, "irrelevant {0} for {1} from {2}", new Object[] {view.getViewName(), this, uri}); + } + } else { + LOGGER.log(Level.FINER, "inapplicable {0} for {1} from {2}", new Object[] {last.getObject(), this, uri}); + } + } else { + LOGGER.log(Level.FINER, "no ancestors for {0} from {1}", new Object[] {this, uri}); + } + } else { + LOGGER.log(Level.FINER, "no current request for {0}", this); } - // otherwise compute the path normally - return getParent().getUrl()+getShortUrl(); + String base = getParent().getUrl(); + LOGGER.log(Level.FINER, "falling back to {0}{1} for {2} from {3}", new Object[] {base, shortUrl, this, uri}); + return base + shortUrl; } public String getShortUrl() { @@ -484,8 +520,23 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet @RequirePOST public void doDoDelete( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, InterruptedException { delete(); - if (rsp != null) // null for CLI - rsp.sendRedirect2(req.getContextPath()+"/"+getParent().getUrl()); + if (req == null || rsp == null) { // CLI + return; + } + List ancestors = req.getAncestors(); + ListIterator it = ancestors.listIterator(ancestors.size()); + String url = getParent().getUrl(); // fallback but we ought to get to Jenkins.instance at the root + while (it.hasPrevious()) { + Object a = it.previous().getObject(); + if (a instanceof View) { + url = ((View) a).getUrl(); + break; + } else if (a instanceof ViewGroup && a != this) { + url = ((ViewGroup) a).getUrl(); + break; + } + } + rsp.sendRedirect2(req.getContextPath() + '/' + url); } public void delete( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { @@ -504,16 +555,12 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet * Any exception indicates the deletion has failed, but {@link AbortException} would prevent the caller * from showing the stack trace. This */ - public synchronized void delete() throws IOException, InterruptedException { + public void delete() throws IOException, InterruptedException { checkPermission(DELETE); - performDelete(); - - try { - invokeOnDeleted(); - } catch (AbstractMethodError e) { - // ignore - } - + synchronized (this) { // could just make performDelete synchronized but overriders might not honor that + performDelete(); + } // JENKINS-19446: leave synch block, but JENKINS-22001: still notify synchronously + invokeOnDeleted(); Jenkins.getInstance().rebuildDependencyGraphAsync(); } @@ -522,7 +569,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet * on BugParade for more details. */ private void invokeOnDeleted() throws IOException { - getParent().onDeleted(this); + getParent().onDeleted(AbstractItem.this); } /** @@ -566,6 +613,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet /** * Updates Job by its XML definition. + * @since 1.473 */ public void updateByXml(Source source) throws IOException { checkPermission(CONFIGURE); @@ -582,17 +630,17 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet new StreamResult(out)); out.close(); } catch (TransformerException e) { - throw new IOException2("Failed to persist configuration.xml", e); + throw new IOException("Failed to persist config.xml", e); } // try to reflect the changes by reloading new XmlFile(Items.XSTREAM, out.getTemporaryFile()).unmarshal(this); - Items.updatingByXml.set(true); - try { - onLoad(getParent(), getRootDir().getName()); - } finally { - Items.updatingByXml.set(false); - } + Items.whileUpdatingByXml(new Callable() { + @Override public Void call() throws IOException { + onLoad(getParent(), getRootDir().getName()); + return null; + } + }); Jenkins.getInstance().rebuildDependencyGraphAsync(); // if everything went well, commit this new version @@ -603,6 +651,34 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet } } + /** + * Reloads this job from the disk. + * + * Exposed through CLI as well. + * + * TODO: think about exposing this to UI + * + * @since 1.556 + */ + @CLIMethod(name="reload-job") + @RequirePOST + public void doReload() throws IOException { + checkPermission(CONFIGURE); + + // try to reflect the changes by reloading + getConfigFile().unmarshal(this); + Items.whileUpdatingByXml(new Callable() { + @Override + public Void call() throws IOException { + onLoad(getParent(), getRootDir().getName()); + return null; + } + }); + Jenkins.getInstance().rebuildDependencyGraphAsync(); + + SaveableListener.fireOnChange(this, getConfigFile()); + } + /* (non-Javadoc) * @see hudson.model.AbstractModelObject#getSearchName() @@ -615,8 +691,8 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet return getName(); } - public String toString() { - return super.toString()+'['+getFullName()+']'; + @Override public String toString() { + return super.toString() + '[' + (parent != null ? getFullName() : "?/" + name) + ']'; } /** @@ -625,6 +701,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet @CLIResolver public static AbstractItem resolveForCLI( @Argument(required=true,metaVar="NAME",usage="Job name") String name) throws CmdLineException { + // TODO can this (and its pseudo-override in AbstractProject) share code with GenericItemOptionHandler, used for explicit CLICommand’s rather than CLIMethod’s? AbstractItem item = Jenkins.getInstance().getItemByFullName(name, AbstractItem.class); if (item==null) throw new CmdLineException(null,Messages.AbstractItem_NoSuchJobExists(name,AbstractProject.findNearest(name).getFullName())); diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index 9a47772346c032495c8dc990532e7515a9b15fb2..5836a00e26e9949c0a9463848654983416c7e2cf 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -27,47 +27,48 @@ */ package hudson.model; -import com.infradna.tool.bridge_method_injector.WithBridgeMethods; -import hudson.EnvVars; -import hudson.Functions; import antlr.ANTLRException; +import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import hudson.AbortException; import hudson.CopyOnWrite; +import hudson.EnvVars; +import hudson.ExtensionPoint; import hudson.FeedAdapter; import hudson.FilePath; +import hudson.Functions; import hudson.Launcher; import hudson.Util; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; import hudson.model.Cause.LegacyCodeCause; -import hudson.model.Cause.RemoteCause; -import hudson.model.Cause.UserIdCause; import hudson.model.Descriptor.FormException; import hudson.model.Fingerprint.RangeSet; +import hudson.model.Node.Mode; import hudson.model.PermalinkProjectAction.Permalink; import hudson.model.Queue.Executable; import hudson.model.Queue.Task; -import hudson.model.queue.QueueTaskFuture; -import hudson.model.queue.ScheduleResult; -import hudson.model.queue.SubTask; -import hudson.model.RunMap.Constructor; import hudson.model.labels.LabelAtom; import hudson.model.labels.LabelExpression; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SCMPollListener; import hudson.model.queue.CauseOfBlockage; +import hudson.model.queue.QueueTaskFuture; +import hudson.model.queue.SubTask; import hudson.model.queue.SubTaskContributor; import hudson.node_monitors.DiskSpaceMonitor; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import hudson.scm.NullSCM; import hudson.scm.PollingResult; + +import static hudson.scm.PollingResult.*; import hudson.scm.SCM; import hudson.scm.SCMRevisionState; import hudson.scm.SCMS; import hudson.search.SearchIndexBuilder; import hudson.security.ACL; import hudson.security.Permission; +import hudson.slaves.Cloud; import hudson.slaves.WorkspaceList; import hudson.tasks.BuildStep; import hudson.tasks.BuildStepDescriptor; @@ -80,67 +81,58 @@ import hudson.triggers.TriggerDescriptor; import hudson.util.AlternativeUiTextProvider; import hudson.util.AlternativeUiTextProvider.Message; import hudson.util.DescribableList; -import hudson.util.EditDistance; import hudson.util.FormValidation; -import hudson.widgets.BuildHistoryWidget; +import hudson.util.TimeUnit2; import hudson.widgets.HistoryWidget; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.Vector; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.servlet.ServletException; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; import jenkins.model.ModelObjectWithChildren; -import jenkins.model.lazy.AbstractLazyLoadRunMap.Direction; +import jenkins.model.ParameterizedJobMixIn; +import jenkins.model.Uptime; +import jenkins.model.lazy.LazyBuildMixIn; import jenkins.scm.DefaultSCMCheckoutStrategyImpl; import jenkins.scm.SCMCheckoutStrategy; import jenkins.scm.SCMCheckoutStrategyDescriptor; import jenkins.util.TimeDuration; import net.sf.json.JSONObject; import org.acegisecurity.Authentication; -import org.acegisecurity.context.SecurityContext; -import org.acegisecurity.context.SecurityContextHolder; import org.jenkinsci.bytecode.AdaptField; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.stapler.Ancestor; +import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.ForwardToView; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.interceptor.RequirePOST; -import javax.annotation.Nonnull; -import javax.servlet.ServletException; -import java.io.File; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.Vector; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static hudson.scm.PollingResult.*; -import javax.annotation.CheckForNull; -import static javax.servlet.http.HttpServletResponse.*; - /** * Base implementation of {@link Job}s that build software. * @@ -150,7 +142,7 @@ import static javax.servlet.http.HttpServletResponse.*; * @see AbstractBuild */ @SuppressWarnings("rawtypes") -public abstract class AbstractProject

,R extends AbstractBuild> extends Job implements BuildableItem, ModelObjectWithChildren { +public abstract class AbstractProject

,R extends AbstractBuild> extends Job implements BuildableItem, ModelObjectWithChildren, LazyBuildMixIn.LazyLoadingJob, ParameterizedJobMixIn.ParameterizedJob { /** * {@link SCM} associated with the project. @@ -169,15 +161,16 @@ public abstract class AbstractProject

,R extends A */ private volatile transient SCMRevisionState pollingBaseline = null; + private transient LazyBuildMixIn buildMixIn; + /** * All the builds keyed by their build number. - * + * Kept here for binary compatibility only; otherwise use {@link #buildMixIn}. * External code should use {@link #getBuildByNumber(int)} or {@link #getLastBuild()} and traverse via * {@link Run#getPreviousBuild()} */ @Restricted(NoExternalUse.class) - @SuppressWarnings("deprecation") // [JENKINS-15156] builds accessed before onLoad or onCreatedFromScratch called - protected transient RunMap builds = new RunMap(); + protected transient RunMap builds; /** * The quiet period. Null to delegate to the system default. @@ -270,6 +263,8 @@ public abstract class AbstractProject

,R extends A protected AbstractProject(ItemGroup parent, String name) { super(parent,name); + buildMixIn = createBuildMixIn(); + builds = buildMixIn.getRunMap(); if(Jenkins.getInstance()!=null && !Jenkins.getInstance().getNodes().isEmpty()) { // if a new job is configured with Hudson that already has slave nodes @@ -278,6 +273,31 @@ public abstract class AbstractProject

,R extends A } } + private LazyBuildMixIn createBuildMixIn() { + return new LazyBuildMixIn() { + @SuppressWarnings("unchecked") // untypable + @Override protected P asJob() { + return (P) AbstractProject.this; + } + @Override protected Class getBuildClass() { + return AbstractProject.this.getBuildClass(); + } + }; + } + + @Override public LazyBuildMixIn getLazyBuildMixIn() { + return buildMixIn; + } + + private ParameterizedJobMixIn getParameterizedJobMixIn() { + return new ParameterizedJobMixIn() { + @SuppressWarnings("unchecked") // untypable + @Override protected P asJob() { + return (P) AbstractProject.this; + } + }; + } + @Override public synchronized void save() throws IOException { super.save(); @@ -287,7 +307,8 @@ public abstract class AbstractProject

,R extends A @Override public void onCreatedFromScratch() { super.onCreatedFromScratch(); - builds = createBuildRunMap(); + buildMixIn.onCreatedFromScratch(); + builds = buildMixIn.getRunMap(); // solicit initial contributions, especially from TransientProjectActionFactory updateTransientActions(); } @@ -295,36 +316,15 @@ public abstract class AbstractProject

,R extends A @Override public void onLoad(ItemGroup parent, String name) throws IOException { super.onLoad(parent, name); - - RunMap builds = createBuildRunMap(); - - RunMap currentBuilds = this.builds; - - if (currentBuilds==null && parent!=null) { - // are we overwriting what currently exist? - // this is primarily when Jenkins is getting reloaded - Item current; - try { - current = parent.getItem(name); - } catch (RuntimeException x) { - LOGGER.log(Level.WARNING, "failed to look up " + name + " in " + parent, x); - current = null; - } - if (current!=null && current.getClass()==getClass()) { - currentBuilds = ((AbstractProject)current).builds; - } - } - if (currentBuilds !=null) { - // if we are reloading, keep all those that are still building intact - for (R r : currentBuilds.getLoadedBuilds().values()) { - if (r.isBuilding()) - builds.put(r); - } + if (buildMixIn == null) { + buildMixIn = createBuildMixIn(); } - this.builds = builds; + buildMixIn.onLoad(parent, name); + builds = buildMixIn.getRunMap(); triggers().setOwner(this); - for (Trigger t : triggers()) - t.start(this, Items.updatingByXml.get()); + for (Trigger t : triggers()) { + t.start(this, Items.currentlyUpdatingByXml()); + } if(scm==null) scm = new NullSCM(); // perhaps it was pointing to a plugin that no longer exists. @@ -333,14 +333,6 @@ public abstract class AbstractProject

,R extends A updateTransientActions(); } - private RunMap createBuildRunMap() { - return new RunMap(getBuildDir(), new Constructor() { - public R create(File dir) throws IOException { - return loadBuild(dir); - } - }); - } - @WithBridgeMethods(List.class) protected DescribableList,TriggerDescriptor> triggers() { if (triggers == null) { @@ -353,12 +345,14 @@ public abstract class AbstractProject

,R extends A public EnvVars getEnvironment(Node node, TaskListener listener) throws IOException, InterruptedException { EnvVars env = super.getEnvironment(node, listener); - JDK jdk = getJDK(); - if (jdk != null) { + JDK jdkTool = getJDK(); + if (jdkTool != null) { if (node != null) { // just in case were not in a build - jdk = jdk.forNode(node, listener); + jdkTool = jdkTool.forNode(node, listener); } - jdk.buildEnvVars(env); + jdkTool.buildEnvVars(env); + } else if (jdk != null) { + listener.getLogger().println("No JDK named ‘" + jdk + "’ found"); } return env; @@ -396,7 +390,7 @@ public abstract class AbstractProject

,R extends A * If this project is configured to be always built on this node, * return that {@link Node}. Otherwise null. */ - public Label getAssignedLabel() { + public @CheckForNull Label getAssignedLabel() { if(canRoam) return null; @@ -471,7 +465,7 @@ public abstract class AbstractProject

,R extends A * @since 1.401 */ public String getBuildNowText() { - return AlternativeUiTextProvider.get(BUILD_NOW_TEXT, this, isParameterized() ? Messages.AbstractProject_build_with_parameters() : Messages.AbstractProject_BuildNow()); + return AlternativeUiTextProvider.get(BUILD_NOW_TEXT, this, getParameterizedJobMixIn().getBuildNowText()); } /** @@ -730,7 +724,7 @@ public abstract class AbstractProject

,R extends A @Override public BallColor getIconColor() { if(isDisabled()) - return BallColor.DISABLED; + return isBuilding() ? BallColor.DISABLED_ANIME : BallColor.DISABLED; else return super.getIconColor(); } @@ -773,13 +767,7 @@ public abstract class AbstractProject

,R extends A } public List getProminentActions() { - List a = getActions(); - List pa = new Vector(); - for (Action action : a) { - if(action instanceof ProminentProjectAction) - pa.add((ProminentProjectAction) action); - } - return pa; + return getActions(ProminentProjectAction.class); } @Override @@ -788,13 +776,6 @@ public abstract class AbstractProject

,R extends A updateTransientActions(); - Set upstream = Collections.emptySet(); - if(req.getParameter("pseudoUpstreamTrigger")!=null) { - upstream = new HashSet(Items.fromNameList(getParent(),req.getParameter("upstreamProjects"),AbstractProject.class)); - } - - convertUpstreamBuildTrigger(upstream); - // notify the queue as the project might be now tied to different node Jenkins.getInstance().getQueue().scheduleMaintenance(); @@ -802,72 +783,12 @@ public abstract class AbstractProject

,R extends A Jenkins.getInstance().rebuildDependencyGraphAsync(); } - /** - * Reflect the submission of the pseudo 'upstream build trigger'. - */ - /* package */ void convertUpstreamBuildTrigger(Set upstream) throws IOException { - - SecurityContext saveCtx = ACL.impersonate(ACL.SYSTEM); - try { - for (AbstractProject p : Jenkins.getInstance().getAllItems(AbstractProject.class)) { - // Don't consider child projects such as MatrixConfiguration: - if (!p.isConfigurable()) continue; - boolean isUpstream = upstream.contains(p); - synchronized(p) { - // does 'p' include us in its BuildTrigger? - DescribableList> pl = p.getPublishersList(); - BuildTrigger trigger = pl.get(BuildTrigger.class); - List newChildProjects = trigger == null ? new ArrayList():trigger.getChildProjects(p); - if(isUpstream) { - if(!newChildProjects.contains(this)) - newChildProjects.add(this); - } else { - newChildProjects.remove(this); - } - - if(newChildProjects.isEmpty()) { - pl.remove(BuildTrigger.class); - } else { - // here, we just need to replace the old one with the new one, - // but there was a regression (we don't know when it started) that put multiple BuildTriggers - // into the list. For us not to lose the data, we need to merge them all. - List existingList = pl.getAll(BuildTrigger.class); - BuildTrigger existing; - switch (existingList.size()) { - case 0: - existing = null; - break; - case 1: - existing = existingList.get(0); - break; - default: - pl.removeAll(BuildTrigger.class); - Set combinedChildren = new HashSet(); - for (BuildTrigger bt : existingList) - combinedChildren.addAll(bt.getChildProjects(p)); - existing = new BuildTrigger(new ArrayList(combinedChildren),existingList.get(0).getThreshold()); - pl.add(existing); - break; - } - - if(existing!=null && existing.hasSame(p,newChildProjects)) - continue; // no need to touch - pl.replace(new BuildTrigger(newChildProjects, - existing==null? Result.SUCCESS:existing.getThreshold())); - } - } - } - } finally { - SecurityContextHolder.setContext(saveCtx); - } - } - /** * @deprecated * Use {@link #scheduleBuild(Cause)}. Since 1.283 */ public boolean scheduleBuild() { - return scheduleBuild(new LegacyCodeCause()); + return getParameterizedJobMixIn().scheduleBuild(); } /** @@ -875,7 +796,7 @@ public abstract class AbstractProject

,R extends A * Use {@link #scheduleBuild(int, Cause)}. Since 1.283 */ public boolean scheduleBuild(int quietPeriod) { - return scheduleBuild(quietPeriod, new LegacyCodeCause()); + return getParameterizedJobMixIn().scheduleBuild(quietPeriod); } /** @@ -886,11 +807,11 @@ public abstract class AbstractProject

,R extends A * false if the task was rejected from the queue (such as when the system is being shut down.) */ public boolean scheduleBuild(Cause c) { - return scheduleBuild(getQuietPeriod(), c); + return getParameterizedJobMixIn().scheduleBuild(c); } public boolean scheduleBuild(int quietPeriod, Cause c) { - return scheduleBuild(quietPeriod, c, new Action[0]); + return getParameterizedJobMixIn().scheduleBuild(quietPeriod, c); } /** @@ -932,44 +853,11 @@ public abstract class AbstractProject

,R extends A @SuppressWarnings("unchecked") @WithBridgeMethods(Future.class) public QueueTaskFuture scheduleBuild2(int quietPeriod, Cause c, Collection actions) { - if (!isBuildable()) - return null; - List queueActions = new ArrayList(actions); - if (isParameterized() && Util.filter(queueActions, ParametersAction.class).isEmpty()) { - queueActions.add(new ParametersAction(getDefaultParametersValues())); - } - if (c != null) { queueActions.add(new CauseAction(c)); } - - ScheduleResult i = Jenkins.getInstance().getQueue().schedule2(this, quietPeriod, queueActions); - if(i.isAccepted()) - return (QueueTaskFuture)i.getItem().getFuture(); - return null; - } - - private List getDefaultParametersValues() { - ParametersDefinitionProperty paramDefProp = getProperty(ParametersDefinitionProperty.class); - ArrayList defValues = new ArrayList(); - - /* - * This check is made ONLY if someone will call this method even if isParametrized() is false. - */ - if(paramDefProp == null) - return defValues; - - /* Scan for all parameter with an associated default values */ - for(ParameterDefinition paramDefinition : paramDefProp.getParameterDefinitions()) - { - ParameterValue defaultValue = paramDefinition.getDefaultParameterValue(); - - if(defaultValue != null) - defValues.add(defaultValue); - } - - return defValues; + return getParameterizedJobMixIn().scheduleBuild2(quietPeriod, queueActions.toArray(new Action[queueActions.size()])); } /** @@ -1040,18 +928,12 @@ public abstract class AbstractProject

,R extends A @Override public RunMap _getRuns() { - if (builds == null) { - throw new IllegalStateException("no run map created yet for " + this); - } - assert builds.baseDirInitialized() : "neither onCreatedFromScratch nor onLoad called on " + this + " yet"; - return builds; + return buildMixIn._getRuns(); } @Override public void removeRun(R run) { - if (!this.builds.remove(run)) { - LOGGER.log(Level.WARNING, "{0} did not contain {1} to begin with", new Object[] {this, run}); - } + buildMixIn.removeRun(run); } /** @@ -1061,7 +943,7 @@ public abstract class AbstractProject

,R extends A */ @Override public R getBuild(String id) { - return builds.getById(id); + return buildMixIn.getBuild(id); } /** @@ -1071,7 +953,7 @@ public abstract class AbstractProject

,R extends A */ @Override public R getBuildByNumber(int n) { - return builds.getByNumber(n); + return buildMixIn.getBuildByNumber(n); } /** @@ -1081,84 +963,44 @@ public abstract class AbstractProject

,R extends A */ @Override public R getFirstBuild() { - return builds.oldestBuild(); + return buildMixIn.getFirstBuild(); } @Override public @CheckForNull R getLastBuild() { - return builds.newestBuild(); + return buildMixIn.getLastBuild(); } @Override public R getNearestBuild(int n) { - return builds.search(n, Direction.ASC); + return buildMixIn.getNearestBuild(n); } @Override public R getNearestOldBuild(int n) { - return builds.search(n, Direction.DESC); + return buildMixIn.getNearestOldBuild(n); } /** - * Determines Class<R>. + * Type token for the corresponding build type. + * The build class must have two constructors: + * one taking this project type; + * and one taking this project type, then {@link File}. */ protected abstract Class getBuildClass(); - // keep track of the previous time we started a build - private transient long lastBuildStartTime; - /** * Creates a new build of this project for immediate execution. */ protected synchronized R newBuild() throws IOException { - // make sure we don't start two builds in the same second - // so the build directories will be different too - long timeSinceLast = System.currentTimeMillis() - lastBuildStartTime; - if (timeSinceLast < 1000) { - try { - Thread.sleep(1000 - timeSinceLast); - } catch (InterruptedException e) { - } - } - lastBuildStartTime = System.currentTimeMillis(); - try { - R lastBuild = getBuildClass().getConstructor(getClass()).newInstance(this); - builds.put(lastBuild); - return lastBuild; - } catch (InstantiationException e) { - throw new Error(e); - } catch (IllegalAccessException e) { - throw new Error(e); - } catch (InvocationTargetException e) { - throw handleInvocationTargetException(e); - } catch (NoSuchMethodException e) { - throw new Error(e); - } - } - - private IOException handleInvocationTargetException(InvocationTargetException e) { - Throwable t = e.getTargetException(); - if(t instanceof Error) throw (Error)t; - if(t instanceof RuntimeException) throw (RuntimeException)t; - if(t instanceof IOException) return (IOException)t; - throw new Error(t); + return buildMixIn.newBuild(); } /** * Loads an existing build record from disk. */ protected R loadBuild(File dir) throws IOException { - try { - return getBuildClass().getConstructor(getClass(),File.class).newInstance(this,dir); - } catch (InstantiationException e) { - throw new Error(e); - } catch (IllegalAccessException e) { - throw new Error(e); - } catch (InvocationTargetException e) { - throw handleInvocationTargetException(e); - } catch (NoSuchMethodException e) { - throw new Error(e); - } + return buildMixIn.loadBuild(dir); } /** @@ -1171,6 +1013,7 @@ public abstract class AbstractProject

,R extends A * * @see TransientProjectActionFactory */ + @SuppressWarnings("deprecation") @Override public List getActions() { // add all the transient actions, too @@ -1533,7 +1376,26 @@ public abstract class AbstractProject

,R extends A } } - // build now, or nothing will ever be built + // At this point we start thinking about triggering a build just to get a workspace, + // because otherwise there's no way we can detect changes. + // However, first there are some conditions in which we do not want to do so. + // give time for slaves to come online if we are right after reconnection (JENKINS-8408) + long running = Jenkins.getInstance().getInjector().getInstance(Uptime.class).getUptime(); + long remaining = TimeUnit2.MINUTES.toMillis(10)-running; + if (remaining>0 && /* this logic breaks tests of polling */!Functions.getIsUnitTest()) { + listener.getLogger().print(Messages.AbstractProject_AwaitingWorkspaceToComeOnline(remaining/1000)); + listener.getLogger().println( " (" + workspaceOfflineReason.name() + ")"); + return NO_CHANGES; + } + + // Do not trigger build, if no suitable slave is online + if (workspaceOfflineReason.equals(WorkspaceOfflineReason.all_suitable_nodes_are_offline)) { + // No suitable executor is online + listener.getLogger().print(Messages.AbstractProject_AwaitingWorkspaceToComeOnline(running/1000)); + listener.getLogger().println( " (" + workspaceOfflineReason.name() + ")"); + return NO_CHANGES; + } + Label label = getAssignedLabel(); if (label != null && label.isSelfLabel()) { // if the build is fixed on a node, then attempting a build will do us @@ -1542,26 +1404,27 @@ public abstract class AbstractProject

,R extends A listener.getLogger().println( " (" + workspaceOfflineReason.name() + ")"); return NO_CHANGES; } + listener.getLogger().println( ws==null ? Messages.AbstractProject_WorkspaceOffline() : Messages.AbstractProject_NoWorkspace()); if (isInQueue()) { listener.getLogger().println(Messages.AbstractProject_AwaitingBuildForWorkspace()); return NO_CHANGES; - } else { - listener.getLogger().print(Messages.AbstractProject_NewBuildForWorkspace()); - listener.getLogger().println( " (" + workspaceOfflineReason.name() + ")"); - return BUILD_NOW; } + + // build now, or nothing will ever be built + listener.getLogger().print(Messages.AbstractProject_NewBuildForWorkspace()); + listener.getLogger().println( " (" + workspaceOfflineReason.name() + ")"); + return BUILD_NOW; } else { WorkspaceList l = b.getBuiltOn().toComputer().getWorkspaceList(); return pollWithWorkspace(listener, scm, b, ws, l); - } + } else { // polling without workspace LOGGER.fine("Polling SCM changes of " + getName()); - if (pollingBaseline==null) // see NOTE-NO-BASELINE above calcPollingBaseline(getLastBuild(),null,listener); PollingResult r = scm.poll(this, null, null, listener, pollingBaseline); @@ -1596,11 +1459,55 @@ public abstract class AbstractProject

,R extends A enum WorkspaceOfflineReason { nonexisting_workspace, builton_node_gone, - builton_node_no_executors + builton_node_no_executors, + all_suitable_nodes_are_offline, + use_ondemand_slave + } + + /** + * Returns true if all suitable nodes for the job are offline. + * + */ + private boolean isAllSuitableNodesOffline(R build) { + Label label = getAssignedLabel(); + List allNodes = Jenkins.getInstance().getNodes(); + + if (label != null) { + //Invalid label. Put in queue to make administrator fix + if(label.getNodes().isEmpty()) { + return false; + } + //Returns true, if all suitable nodes are offline + return label.isOffline(); + } else { + if(canRoam) { + for (Node n : Jenkins.getInstance().getNodes()) { + Computer c = n.toComputer(); + if (c != null && c.isOnline() && c.isAcceptingTasks() && n.getMode() == Mode.NORMAL) { + // Some executor is online that is ready and this job can run anywhere + return false; + } + } + //We can roam, check that the master is set to be used as much as possible, and not tied jobs only. + if(Jenkins.getInstance().getMode() == Mode.EXCLUSIVE) { + return true; + } else { + return false; + } + } + } + return true; } private WorkspaceOfflineReason workspaceOffline(R build) throws IOException, InterruptedException { FilePath ws = build.getWorkspace(); + Label label = getAssignedLabel(); + + if (isAllSuitableNodesOffline(build)) { + Collection applicableClouds = label == null ? Jenkins.getInstance().clouds : label.getClouds(); + return applicableClouds.isEmpty() ? WorkspaceOfflineReason.all_suitable_nodes_are_offline : WorkspaceOfflineReason.use_ondemand_slave; + } + if (ws==null || !ws.exists()) { return WorkspaceOfflineReason.nonexisting_workspace; } @@ -1675,7 +1582,7 @@ public abstract class AbstractProject

,R extends A } @SuppressWarnings("unchecked") - public Map> getTriggers() { + @Override public Map> getTriggers() { return triggers().toMap(); } @@ -1717,7 +1624,7 @@ public abstract class AbstractProject

,R extends A /** * Returns only those upstream projects that defines {@link BuildTrigger} to this project. * This is a subset of {@link #getUpstreamProjects()} - * + *

No longer used in the UI. * @return A List of upstream projects that has a {@link BuildTrigger} to this project. */ public final List getBuildTriggerUpstreamProjects() { @@ -1789,25 +1696,24 @@ public abstract class AbstractProject

,R extends A /** * Builds the dependency graph. - * @see DependencyGraph + * Since 1.558, not abstract and by default includes dependencies contributed by {@link #triggers()}. */ - protected abstract void buildDependencyGraph(DependencyGraph graph); + protected void buildDependencyGraph(DependencyGraph graph) { + triggers().buildDependencyGraph(this, graph); + } @Override protected SearchIndexBuilder makeSearchIndex() { - SearchIndexBuilder sib = super.makeSearchIndex(); - if(isBuildable() && hasPermission(Jenkins.ADMINISTER)) - sib.add("build","build"); - return sib; + return getParameterizedJobMixIn().extendSearchIndex(super.makeSearchIndex()); } @Override protected HistoryWidget createHistoryWidget() { - return new BuildHistoryWidget(this,builds,HISTORY_ADAPTER); + return buildMixIn.createHistoryWidget(); } public boolean isParameterized() { - return getProperty(ParametersDefinitionProperty.class) != null; + return getParameterizedJobMixIn().isParameterized(); } // @@ -1819,31 +1725,7 @@ public abstract class AbstractProject

,R extends A * Schedules a new build command. */ public void doBuild( StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay ) throws IOException, ServletException { - if (delay==null) delay=new TimeDuration(getQuietPeriod()); - - // if a build is parameterized, let that take over - ParametersDefinitionProperty pp = getProperty(ParametersDefinitionProperty.class); - if (pp != null && !req.getMethod().equals("POST")) { - // show the parameter entry form. - req.getView(pp, "index.jelly").forward(req, rsp); - return; - } - - BuildAuthorizationToken.checkPermission(this, authToken, req, rsp); - - if (pp != null) { - pp._doBuild(req,rsp,delay); - return; - } - - if (!isBuildable()) - throw HttpResponses.error(SC_INTERNAL_SERVER_ERROR,new IOException(getFullName()+" is not buildable")); - - ScheduleResult r = Jenkins.getInstance().getQueue().schedule2(this, delay.getTime(), getBuildCause(req)); - if (r.isAccepted()) { - rsp.sendRedirect(SC_CREATED,req.getContextPath()+'/'+r.getItem().getUrl()); - } else - rsp.sendRedirect("."); + getParameterizedJobMixIn().doBuild(req, rsp, delay); } /** @deprecated use {@link #doBuild(StaplerRequest, StaplerResponse, TimeDuration)} */ @@ -1852,21 +1734,6 @@ public abstract class AbstractProject

,R extends A doBuild(req, rsp, TimeDuration.fromString(req.getParameter("delay"))); } - /** - * Computes the build cause, using RemoteCause or UserCause as appropriate. - */ - /*package*/ CauseAction getBuildCause(StaplerRequest req) { - Cause cause; - if (authToken != null && authToken.getToken() != null && req.getParameter("token") != null) { - // Optional additional cause text when starting via token - String causeText = req.getParameter("cause"); - cause = new RemoteCause(req.getRemoteAddr(), causeText); - } else { - cause = new UserIdCause(); - } - return new CauseAction(cause); - } - /** * Computes the delay by taking the default value and the override in the request parameter into the account. * @@ -1892,15 +1759,7 @@ public abstract class AbstractProject

,R extends A * Currently only String parameters are supported. */ public void doBuildWithParameters(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException { - BuildAuthorizationToken.checkPermission(this, authToken, req, rsp); - - ParametersDefinitionProperty pp = getProperty(ParametersDefinitionProperty.class); - if (pp != null) { - pp.buildWithParameters(req,rsp,delay); - } else { - throw new IllegalStateException("This build is not parameterized!"); - } - + getParameterizedJobMixIn().doBuildWithParameters(req, rsp, delay); } /** @deprecated use {@link #doBuildWithParameters(StaplerRequest, StaplerResponse, TimeDuration)} */ @@ -1913,7 +1772,7 @@ public abstract class AbstractProject

,R extends A * Schedules a new SCM polling command. */ public void doPolling( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { - BuildAuthorizationToken.checkPermission(this, authToken, req, rsp); + BuildAuthorizationToken.checkPermission((Job) this, authToken, req, rsp); schedulePolling(); rsp.sendRedirect("."); } @@ -1923,37 +1782,9 @@ public abstract class AbstractProject

,R extends A */ @RequirePOST public void doCancelQueue( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { - checkPermission(ABORT); - - Jenkins.getInstance().getQueue().cancel(this); - rsp.forwardToPreviousPage(req); + getParameterizedJobMixIn().doCancelQueue(req, rsp); } - /** - * Deletes this project. - */ - @Override - @RequirePOST - public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException { - delete(); - if (req == null || rsp == null) - return; - List ancestors = req.getAncestors(); - ListIterator it = ancestors.listIterator(ancestors.size()); - String url = getParent().getUrl(); // fallback but we ought to get to Jenkins.instance at the root - while (it.hasPrevious()) { - Object a = it.previous().getObject(); - if (a instanceof View) { - url = ((View) a).getUrl(); - break; - } else if (a instanceof ViewGroup) { - url = ((ViewGroup) a).getUrl(); - break; - } - } - rsp.sendRedirect2(req.getContextPath() + '/' + url); - } - @Override protected void submit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException { super.submit(req,rsp); @@ -2006,13 +1837,6 @@ public abstract class AbstractProject

,R extends A triggers.replaceBy(buildDescribable(req, Trigger.for_(this))); for (Trigger t : triggers()) t.start(this,true); - - for (Publisher _t : Descriptor.newInstancesFromHeteroList(req, json, "publisher", Jenkins.getInstance().getExtensionList(BuildTrigger.DescriptorImpl.class))) { - BuildTrigger t = (BuildTrigger) _t; - for (AbstractProject downstream : t.getChildProjects(this)) { - downstream.checkPermission(BUILD); - } - } } /** @@ -2053,7 +1877,7 @@ public abstract class AbstractProject

,R extends A * Serves the workspace files. */ public DirectoryBrowserSupport doWs( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, InterruptedException { - checkPermission(AbstractProject.WORKSPACE); + checkPermission(Item.WORKSPACE); FilePath ws = getSomeWorkspace(); if ((ws == null) || (!ws.exists())) { // if there's no workspace, report a nice error message @@ -2102,6 +1926,7 @@ public abstract class AbstractProject

,R extends A makeDisabled(false); return new HttpRedirect("."); } + /** * RSS feed for changes in this project. @@ -2194,7 +2019,8 @@ public abstract class AbstractProject

,R extends A return true; } - public FormValidation doCheckAssignedLabelString(@QueryParameter String value) { + public FormValidation doCheckAssignedLabelString(@AncestorInPath AbstractProject project, + @QueryParameter String value) { if (Util.fixEmpty(value)==null) return FormValidation.ok(); // nothing typed yet try { @@ -2203,7 +2029,8 @@ public abstract class AbstractProject

,R extends A return FormValidation.error(e, Messages.AbstractProject_AssignedLabelString_InvalidBooleanExpression(e.getMessage())); } - Label l = Jenkins.getInstance().getLabel(value); + Jenkins j = Jenkins.getInstance(); + Label l = j.getLabel(value); if (l.isEmpty()) { for (LabelAtom a : l.listAtoms()) { if (a.isEmpty()) { @@ -2213,7 +2040,18 @@ public abstract class AbstractProject

,R extends A } return FormValidation.warning(Messages.AbstractProject_AssignedLabelString_NoMatch()); } - return FormValidation.ok(); + if (project != null) { + for (AbstractProject.LabelValidator v : j + .getExtensionList(AbstractProject.LabelValidator.class)) { + FormValidation result = v.check(project, l); + if (!FormValidation.Kind.OK.equals(result.kind)) { + return result; + } + } + } + return FormValidation.okWithMarkup(Messages.AbstractProject_LabelLink( + j.getRootUrl(), l.getUrl(), l.getNodes().size() + l.getClouds().size() + )); } public FormValidation doCheckCustomWorkspace(@QueryParameter(value="customWorkspace.directory") String customWorkspace){ @@ -2299,24 +2137,20 @@ public abstract class AbstractProject

,R extends A /** * Finds a {@link AbstractProject} that has the name closest to the given name. + * @see Items#findNearest */ - public static AbstractProject findNearest(String name) { - return findNearest(name,Hudson.getInstance()); + public static @CheckForNull AbstractProject findNearest(String name) { + return findNearest(name,Jenkins.getInstance()); } /** * Finds a {@link AbstractProject} whose name (when referenced from the specified context) is closest to the given name. * * @since 1.419 + * @see Items#findNearest */ - public static AbstractProject findNearest(String name, ItemGroup context) { - List projects = Hudson.getInstance().getAllItems(AbstractProject.class); - String[] names = new String[projects.size()]; - for( int i=0; i REVERSE_INTEGER_COMPARATOR = new Comparator() { @@ -2373,5 +2207,23 @@ public abstract class AbstractProject

,R extends A this.customWorkspace= Util.fixEmptyAndTrim(customWorkspace); save(); } - + + /** + * Plugins may want to contribute additional restrictions on the use of specific labels for specific projects. + * This extension point allows such restrictions. + * + * @since 1.540 + */ + public static abstract class LabelValidator implements ExtensionPoint { + /** + * Check the use of the label within the specified context. + * + * @param project the project that wants to restrict itself to the specified label. + * @param label the label that the project wants to restrict itself to. + * @return the {@link FormValidation} result. + */ + @Nonnull + public abstract FormValidation check(@Nonnull AbstractProject project, @Nonnull Label label); + } + } diff --git a/core/src/main/java/hudson/model/Action.java b/core/src/main/java/hudson/model/Action.java index 3687141f33f8492d1886f7d3b70539a5ebf9aa02..ed9862bc6fad6dd60bb3f6a77b4b8857af9336e5 100644 --- a/core/src/main/java/hudson/model/Action.java +++ b/core/src/main/java/hudson/model/Action.java @@ -104,7 +104,7 @@ public interface Action extends ModelObject { /** * Gets the URL path name. * - *

tions + *

* For example, if this method returns "xyz", and if the parent object * (that this action is associated with) is bound to /foo/bar/zot, * then this action object will be exposed to /foo/bar/zot/xyz. diff --git a/core/src/main/java/hudson/model/Actionable.java b/core/src/main/java/hudson/model/Actionable.java index 01403838c064e3ef40745ed2ac6e79b2aa8ca060..03ac14220e378ee1b160f7ea3c61ba54f366a028 100644 --- a/core/src/main/java/hudson/model/Actionable.java +++ b/core/src/main/java/hudson/model/Actionable.java @@ -23,27 +23,19 @@ */ package hudson.model; -import hudson.Functions; -import hudson.model.queue.Tasks; +import hudson.Util; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithContextMenu; -import org.apache.commons.jelly.JellyContext; -import org.apache.commons.jelly.JellyException; -import org.apache.commons.jelly.JellyTagException; -import org.apache.commons.jelly.Script; -import org.apache.commons.jelly.XMLOutput; +import jenkins.model.TransientActionFactory; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.WebApp; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; -import org.kohsuke.stapler.jelly.JellyClassTearOff; -import org.kohsuke.stapler.jelly.JellyFacet; -import org.xml.sax.helpers.DefaultHandler; - -import java.util.ArrayList; -import java.util.List; -import java.util.Vector; -import java.util.concurrent.CopyOnWriteArrayList; /** * {@link ModelObject} that can have additional {@link Action}s. @@ -60,15 +52,23 @@ public abstract class Actionable extends AbstractModelObject implements ModelObj private volatile CopyOnWriteArrayList actions; /** - * Gets actions contributed to this build. + * Gets actions contributed to this object. * *

- * A new {@link Action} can be added by {@code getActions().add(...)}. + * A new {@link Action} can be added by {@link #addAction}. + * + *

If you are reading the list, rather than modifying it, + * use {@link #getAllActions} instead. + * This method by default returns only persistent actions + * (though some subclasses override it to return an extended unmodifiable list). * * @return * may be empty but never null. + * @deprecated Normally outside code should not call this method any more. + * Use {@link #getAllActions}, or {@link #addAction}, or {@link #replaceAction}. + * May still be called for compatibility reasons from subclasses predating {@link TransientActionFactory}. */ - @Exported + @Deprecated public List getActions() { if(actions == null) { synchronized (this) { @@ -81,7 +81,30 @@ public abstract class Actionable extends AbstractModelObject implements ModelObj } /** - * Gets all actions of a specified type that contributed to this build. + * Gets all actions, transient or persistent. + * {@link #getActions} is supplemented with anything contributed by {@link TransientActionFactory}. + * @return an unmodifiable, possible empty list + * @since 1.548 + */ + @Exported(name="actions") + public final List getAllActions() { + List _actions = new ArrayList(getActions()); + Jenkins jenkins = Jenkins.getInstance(); + if (jenkins != null) { + for (TransientActionFactory taf : jenkins.getExtensionList(TransientActionFactory.class)) { + if (taf.type().isInstance(this)) { + _actions.addAll(createFor(taf)); + } + } + } + return Collections.unmodifiableList(_actions); + } + private Collection createFor(TransientActionFactory taf) { + return taf.createFor(taf.type().cast(this)); + } + + /** + * Gets all actions of a specified type that contributed to this object. * * @param type The type of action to return. * @return @@ -89,23 +112,39 @@ public abstract class Actionable extends AbstractModelObject implements ModelObj * @see #getAction(Class) */ public List getActions(Class type) { - List result = new Vector(); - for (Action a : getActions()) - if (type.isInstance(a)) - result.add(type.cast(a)); - return result; + return Util.filter(getAllActions(), type); } /** * Adds a new action. * - * Short for getActions().add(a) + * The default implementation calls {@code getActions().add(a)}. */ public void addAction(Action a) { if(a==null) throw new IllegalArgumentException(); getActions().add(a); } + /** + * Add an action, replacing any existing action of the (exact) same class. + * @param a an action to add/replace + * @since 1.548 + */ + public void replaceAction(Action a) { + // CopyOnWriteArrayList does not support Iterator.remove, so need to do it this way: + List old = new ArrayList(1); + List current = getActions(); + for (Action a2 : current) { + if (a2.getClass() == a.getClass()) { + old.add(a2); + } + } + current.removeAll(old); + addAction(a); + } + + /** @deprecated No clear purpose, since subclasses may have overridden {@link #getActions}, and does not consider {@link TransientActionFactory}. */ + @Deprecated public Action getAction(int index) { if(actions==null) return null; return actions.get(index); @@ -119,14 +158,14 @@ public abstract class Actionable extends AbstractModelObject implements ModelObj * @see #getActions(Class) */ public T getAction(Class type) { - for (Action a : getActions()) + for (Action a : getAllActions()) if (type.isInstance(a)) return type.cast(a); return null; } public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { - for (Action a : getActions()) { + for (Action a : getAllActions()) { if(a==null) continue; // be defensive String urlName = a.getUrlName(); @@ -138,7 +177,7 @@ public abstract class Actionable extends AbstractModelObject implements ModelObj return null; } - public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { + @Override public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { return new ContextMenu().from(this,request,response); } } diff --git a/core/src/main/java/hudson/model/AllView.java b/core/src/main/java/hudson/model/AllView.java index 5d9b235769b7c75ad3aa1cd5a061a8b5af95b07b..6cf8f2448d7c10734a06b54dbba4f3cf407ec92b 100644 --- a/core/src/main/java/hudson/model/AllView.java +++ b/core/src/main/java/hudson/model/AllView.java @@ -23,7 +23,6 @@ */ package hudson.model; -import jenkins.model.Jenkins; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; @@ -35,6 +34,7 @@ import java.util.Collection; import hudson.model.Descriptor.FormException; import hudson.Extension; +import org.kohsuke.stapler.interceptor.RequirePOST; /** * {@link View} that contains everything. @@ -53,11 +53,6 @@ public class AllView extends View { this.owner = owner; } - @Override - public String getDescription() { - return Jenkins.getInstance().getDescription(); - } - @Override public boolean isEditable() { return false; @@ -68,6 +63,7 @@ public class AllView extends View { return true; } + @RequirePOST @Override public Item doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { @@ -82,24 +78,11 @@ public class AllView extends View { return (Collection)getOwnerItemGroup().getItems(); } - @Override - public synchronized void doSubmitDescription( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { - checkPermission(Jenkins.ADMINISTER); - - Jenkins.getInstance().setSystemMessage(req.getParameter("description")); - rsp.sendRedirect("."); - } - @Override public String getPostConstructLandingPage() { return ""; // there's no configuration page } - @Override - public void onJobRenamed(Item item, String oldName, String newName) { - // noop - } - @Override protected void submit(StaplerRequest req) throws IOException, ServletException, FormException { // noop diff --git a/core/src/main/java/hudson/model/AperiodicWork.java b/core/src/main/java/hudson/model/AperiodicWork.java index 512db0efc0e0f4cf3af11f003790c2c03c6f6171..90173f2e893ce212bf072bf4b0cc889935cf7d4e 100644 --- a/core/src/main/java/hudson/model/AperiodicWork.java +++ b/core/src/main/java/hudson/model/AperiodicWork.java @@ -25,14 +25,17 @@ package hudson.model; import hudson.ExtensionList; import hudson.ExtensionPoint; +import hudson.init.Initializer; import hudson.triggers.SafeTimerTask; -import hudson.triggers.Trigger; import jenkins.model.Jenkins; +import jenkins.util.Timer; import java.util.Random; -import java.util.Timer; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.logging.Logger; +import static hudson.init.InitMilestone.JOB_LOADED; /** @@ -85,12 +88,17 @@ public abstract class AperiodicWork extends SafeTimerTask implements ExtensionPo @Override public final void doRun() throws Exception{ doAperiodicRun(); - Timer timer = Trigger.timer; - if (timer != null) { - timer.schedule(getNewInstance(), getRecurrencePeriod()); + Timer.get().schedule(getNewInstance(), getRecurrencePeriod(), TimeUnit.MILLISECONDS); + } + + @Initializer(after= JOB_LOADED) + public static void init() { + // start all AperidocWorks + for (AperiodicWork p : AperiodicWork.all()) { + Timer.get().schedule(p, p.getInitialDelay(), TimeUnit.MILLISECONDS); } } - + protected abstract void doAperiodicRun(); /** diff --git a/core/src/main/java/hudson/model/Api.java b/core/src/main/java/hudson/model/Api.java index 5bb9b8f8af1f6383551a0c7bc26eecb20197f485..ff84f4b66d2a9ce39bc82ceac11963fd43e77c8f 100644 --- a/core/src/main/java/hudson/model/Api.java +++ b/core/src/main/java/hudson/model/Api.java @@ -23,8 +23,9 @@ */ package hudson.model; -import hudson.util.IOException2; import jenkins.model.Jenkins; +import jenkins.security.SecureRequester; + import org.dom4j.CharacterData; import org.dom4j.Document; import org.dom4j.DocumentException; @@ -59,6 +60,7 @@ import java.util.logging.Logger; * * @author Kohsuke Kawaguchi * @see Exported + * @see SecureRequester */ public class Api extends AbstractModelObject { /** @@ -149,19 +151,24 @@ public class Api extends AbstractModelObject { } catch (DocumentException e) { LOGGER.log(Level.FINER, "Failed to do XPath/wrapper handling. XML is as follows:"+sw, e); - throw new IOException2("Failed to do XPath/wrapper handling. Turn on FINER logging to view XML.",e); + throw new IOException("Failed to do XPath/wrapper handling. Turn on FINER logging to view XML.",e); } + + if (isSimpleOutput(result) && !permit(req)) { + // simple output prohibited + rsp.sendError(HttpURLConnection.HTTP_FORBIDDEN, "primitive XPath result sets forbidden; implement jenkins.security.SecureRequester"); + return; + } + + // switch to gzipped output OutputStream o = rsp.getCompressedOutputStream(req); try { - if (result instanceof CharacterData || result instanceof String || result instanceof Number || result instanceof Boolean) { - if (INSECURE) { - rsp.setContentType("text/plain;charset=UTF-8"); - String text = result instanceof CharacterData ? ((CharacterData) result).getText() : result.toString(); - o.write(text.getBytes("UTF-8")); - } else { - rsp.sendError(HttpURLConnection.HTTP_FORBIDDEN, "primitive XPath result sets forbidden; can use -Dhudson.model.Api.INSECURE=true if you run without security"); - } + if (isSimpleOutput(result)) { + // simple output allowed + rsp.setContentType("text/plain;charset=UTF-8"); + String text = result instanceof CharacterData ? ((CharacterData) result).getText() : result.toString(); + o.write(text.getBytes("UTF-8")); return; } @@ -173,6 +180,10 @@ public class Api extends AbstractModelObject { } } + private boolean isSimpleOutput(Object result) { + return result instanceof CharacterData || result instanceof String || result instanceof Number || result instanceof Boolean; + } + /** * Generate schema. */ @@ -188,11 +199,11 @@ public class Api extends AbstractModelObject { * Exposes the bean as JSON. */ public void doJson(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { - if (INSECURE || req.getParameter("jsonp") == null) { - setHeaders(rsp); + if (req.getParameter("jsonp") == null || permit(req)) { + setHeaders(rsp); rsp.serveExposedBean(req,bean, Flavor.JSON); } else { - rsp.sendError(HttpURLConnection.HTTP_FORBIDDEN, "jsonp forbidden; can use -Dhudson.model.Api.INSECURE=true if you run without security"); + rsp.sendError(HttpURLConnection.HTTP_FORBIDDEN, "jsonp forbidden; implement jenkins.security.SecureRequester"); } } @@ -204,6 +215,15 @@ public class Api extends AbstractModelObject { rsp.serveExposedBean(req,bean, Flavor.PYTHON); } + private boolean permit(StaplerRequest req) { + for (SecureRequester r : Jenkins.getInstance().getExtensionList(SecureRequester.class)) { + if (r.permit(req, bean)) { + return true; + } + } + return false; + } + private void setHeaders(StaplerResponse rsp) { rsp.setHeader("X-Jenkins", Jenkins.VERSION); rsp.setHeader("X-Jenkins-Session", Jenkins.SESSION_HASH); @@ -211,6 +231,5 @@ public class Api extends AbstractModelObject { private static final Logger LOGGER = Logger.getLogger(Api.class.getName()); private static final ModelBuilder MODEL_BUILDER = new ModelBuilder(); - private static final boolean INSECURE = "true".equals(System.getProperty("hudson.model.Api.INSECURE")); } diff --git a/core/src/main/java/hudson/model/AsyncPeriodicWork.java b/core/src/main/java/hudson/model/AsyncPeriodicWork.java index f678e1425eaf8dfbaf9b9b7136f049ef4bb65813..9444887b5b1c262818920df69006021e291e5559 100644 --- a/core/src/main/java/hudson/model/AsyncPeriodicWork.java +++ b/core/src/main/java/hudson/model/AsyncPeriodicWork.java @@ -3,12 +3,11 @@ package hudson.model; import hudson.security.ACL; import hudson.util.StreamTaskListener; import jenkins.model.Jenkins; -import org.acegisecurity.context.SecurityContext; -import org.acegisecurity.context.SecurityContextHolder; import java.io.File; import java.io.IOException; import java.util.logging.Level; +import java.util.logging.LogRecord; /** * {@link PeriodicWork} that takes a long time to run. @@ -35,15 +34,16 @@ public abstract class AsyncPeriodicWork extends PeriodicWork { /** * Schedules this periodic work now in a new thread, if one isn't already running. */ + @SuppressWarnings("deprecation") // in this case we really want to use PeriodicWork.logger since it reports the impl class public final void doRun() { try { if(thread!=null && thread.isAlive()) { - logger.log(Level.INFO, name+" thread is still running. Execution aborted."); + logger.log(this.getSlowLoggingLevel(), "{0} thread is still running. Execution aborted.", name); return; } thread = new Thread(new Runnable() { public void run() { - logger.log(Level.INFO, "Started "+name); + logger.log(getNormalLoggingLevel(), "Started {0}", name); long startTime = System.currentTimeMillis(); StreamTaskListener l = createListener(); @@ -59,13 +59,16 @@ public abstract class AsyncPeriodicWork extends PeriodicWork { l.closeQuietly(); } - logger.log(Level.INFO, "Finished "+name+". "+ - (System.currentTimeMillis()-startTime)+" ms"); + logger.log(getNormalLoggingLevel(), "Finished {0}. {1,number} ms", + new Object[]{name, (System.currentTimeMillis()-startTime)}); } },name+" thread"); thread.start(); } catch (Throwable t) { - logger.log(Level.SEVERE, name+" thread failed with error", t); + LogRecord lr = new LogRecord(this.getErrorLoggingLevel(), "{0} thread failed with error"); + lr.setThrown(t); + lr.setParameters(new Object[]{name}); + logger.log(lr); } } @@ -83,7 +86,43 @@ public abstract class AsyncPeriodicWork extends PeriodicWork { protected File getLogFile() { return new File(Jenkins.getInstance().getRootDir(),name+".log"); } + + /** + * Returns the logging level at which normal messages are displayed. + * + * @return + * The logging level as @Level. + * + * @since 1.551 + */ + protected Level getNormalLoggingLevel() { + return Level.INFO; + } + + /** + * Returns the logging level at which previous task still executing messages is displayed. + * + * @return + * The logging level as @Level. + * + * @since 1.565 + */ + protected Level getSlowLoggingLevel() { + return getNormalLoggingLevel(); + } + /** + * Returns the logging level at which error messages are displayed. + * + * @return + * The logging level as @Level. + * + * @since 1.551 + */ + protected Level getErrorLoggingLevel() { + return Level.SEVERE; + } + /** * Executes the task. * diff --git a/core/src/main/java/hudson/model/AutoCompletionCandidates.java b/core/src/main/java/hudson/model/AutoCompletionCandidates.java index 556ea22806a53d919094bff5a828ab1479bb0de2..0373b552bb007f560a566f63573db9ce683296b2 100644 --- a/core/src/main/java/hudson/model/AutoCompletionCandidates.java +++ b/core/src/main/java/hudson/model/AutoCompletionCandidates.java @@ -91,7 +91,22 @@ public class AutoCompletionCandidates implements HttpResponse { public static AutoCompletionCandidates ofJobNames(final Class type, final String value, @CheckForNull Item self, ItemGroup container) { if (self==container) container = self.getParent(); + return ofJobNames(type, value, container); + } + + /** + * Auto-completes possible job names. + * + * @param type + * Limit the auto-completion to the subtype of this type. + * @param value + * The value the user has typed in. Matched as a prefix. + * @param container + * The nearby contextual {@link ItemGroup} to resolve relative job names from. + * @since 1.553 + */ + public static AutoCompletionCandidates ofJobNames(final Class type, final String value, ItemGroup container) { final AutoCompletionCandidates candidates = new AutoCompletionCandidates(); class Visitor extends ItemVisitor { String prefix; diff --git a/core/src/main/java/hudson/model/BallColor.java b/core/src/main/java/hudson/model/BallColor.java index 17d468ec72954e2a4d3b7ae3325c305b537ef0ee..1554d1ebe19ef03d06dac5baedacfbcfba724d37 100644 --- a/core/src/main/java/hudson/model/BallColor.java +++ b/core/src/main/java/hudson/model/BallColor.java @@ -63,12 +63,12 @@ public enum BallColor implements StatusIcon { GREY("grey",Messages._BallColor_Pending(), ColorPalette.GREY), GREY_ANIME("grey_anime",Messages._BallColor_InProgress(), ColorPalette.GREY), - DISABLED("grey",Messages._BallColor_Disabled(), ColorPalette.GREY), - DISABLED_ANIME("grey_anime",Messages._BallColor_InProgress(), ColorPalette.GREY), - ABORTED("grey",Messages._BallColor_Aborted(), ColorPalette.GREY), - ABORTED_ANIME("grey_anime",Messages._BallColor_InProgress(), ColorPalette.GREY), - NOTBUILT("grey",Messages._BallColor_NotBuilt(), ColorPalette.GREY), - NOTBUILT_ANIME("grey_anime",Messages._BallColor_InProgress(), ColorPalette.GREY), + DISABLED("disabled",Messages._BallColor_Disabled(), ColorPalette.GREY), + DISABLED_ANIME("disabled_anime",Messages._BallColor_InProgress(), ColorPalette.GREY), + ABORTED("aborted",Messages._BallColor_Aborted(), ColorPalette.GREY), + ABORTED_ANIME("aborted_anime",Messages._BallColor_InProgress(), ColorPalette.GREY), + NOTBUILT("nobuilt",Messages._BallColor_NotBuilt(), ColorPalette.GREY), + NOTBUILT_ANIME("nobuilt_anime",Messages._BallColor_InProgress(), ColorPalette.GREY), ; private final Localizable description; diff --git a/core/src/main/java/hudson/model/BooleanParameterDefinition.java b/core/src/main/java/hudson/model/BooleanParameterDefinition.java old mode 100755 new mode 100644 diff --git a/core/src/main/java/hudson/model/BooleanParameterValue.java b/core/src/main/java/hudson/model/BooleanParameterValue.java old mode 100755 new mode 100644 index 3caac0581301c91b404449386c6d1f5cb7de61ee..9b7b94046ba5fef161ed2e795c37a429b35fdbfb --- a/core/src/main/java/hudson/model/BooleanParameterValue.java +++ b/core/src/main/java/hudson/model/BooleanParameterValue.java @@ -48,11 +48,16 @@ public class BooleanParameterValue extends ParameterValue { this.value = value; } + @Override + public Boolean getValue() { + return value; + } + /** * Exposes the name/value as an environment variable. */ @Override - public void buildEnvVars(AbstractBuild build, EnvVars env) { + public void buildEnvironment(Run build, EnvVars env) { env.put(name,Boolean.toString(value)); env.put(name.toUpperCase(Locale.ENGLISH),Boolean.toString(value)); // backward compatibility pre 1.345 } diff --git a/core/src/main/java/hudson/model/Build.java b/core/src/main/java/hudson/model/Build.java index 94e2df5cef5955f3b0c119b27f5c55bebaeaf682..99073f1b7332b5eccc737e2eea1edd55ed5f9a14 100644 --- a/core/src/main/java/hudson/model/Build.java +++ b/core/src/main/java/hudson/model/Build.java @@ -34,15 +34,15 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; import java.io.File; import java.io.IOException; -import java.text.MessageFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.List; import java.util.logging.Logger; +import java.util.logging.Level; -import static hudson.model.Result.ABORTED; import static hudson.model.Result.FAILURE; +import javax.annotation.Nonnull; /** * A build of a {@link Project}. @@ -136,7 +136,7 @@ public abstract class Build

,B extends Build> deprecated class here. */ - protected Result doRun(BuildListener listener) throws Exception { + protected Result doRun(@Nonnull BuildListener listener) throws Exception { if(!preBuild(listener,project.getBuilders())) return FAILURE; if(!preBuild(listener,project.getPublishersList())) @@ -179,7 +179,7 @@ public abstract class Build

,B extends Build> return r; } - public void post2(BuildListener listener) throws IOException, InterruptedException { + public void post2(@Nonnull BuildListener listener) throws IOException, InterruptedException { if (!performAllBuildSteps(listener, project.getPublishersList(), true)) setResult(FAILURE); if (!performAllBuildSteps(listener, project.getProperties(), true)) @@ -187,17 +187,17 @@ public abstract class Build

,B extends Build> } @Override - public void cleanUp(BuildListener listener) throws Exception { + public void cleanUp(@Nonnull BuildListener listener) throws Exception { // at this point it's too late to mark the build as a failure, so ignore return value. performAllBuildSteps(listener, project.getPublishersList(), false); performAllBuildSteps(listener, project.getProperties(), false); super.cleanUp(listener); } - private boolean build(BuildListener listener, Collection steps) throws IOException, InterruptedException { + private boolean build(@Nonnull BuildListener listener, @Nonnull Collection steps) throws IOException, InterruptedException { for( BuildStep bs : steps ) { if(!perform(bs,listener)) { - LOGGER.fine(MessageFormat.format("{0} : {1} failed", Build.this.toString(), bs)); + LOGGER.log(Level.FINE, "{0} : {1} failed", new Object[] {Build.this, bs}); return false; } diff --git a/core/src/main/java/hudson/model/BuildAuthorizationToken.java b/core/src/main/java/hudson/model/BuildAuthorizationToken.java index 8d4483ba084652ee4c6bc7443a5f346f1db34da7..7ab272afdad7f2638a8befa703561aa723a44e1f 100644 --- a/core/src/main/java/hudson/model/BuildAuthorizationToken.java +++ b/core/src/main/java/hudson/model/BuildAuthorizationToken.java @@ -42,7 +42,7 @@ import org.kohsuke.stapler.HttpResponses; * @author Kohsuke Kawaguchi * @see BuildableItem * @deprecated 2008-07-20 - * Use {@link ACL} and {@link AbstractProject#BUILD}. This code is only here + * Use {@link ACL} and {@link Item#BUILD}. This code is only here * for the backward compatibility. */ public final class BuildAuthorizationToken { @@ -62,7 +62,12 @@ public final class BuildAuthorizationToken { return null; } - public static void checkPermission(AbstractProject project, BuildAuthorizationToken token, StaplerRequest req, StaplerResponse rsp) throws IOException { + @Deprecated public static void checkPermission(AbstractProject project, BuildAuthorizationToken token, StaplerRequest req, StaplerResponse rsp) throws IOException { + Job j = project; + checkPermission(j, token, req, rsp); + } + + public static void checkPermission(Job project, BuildAuthorizationToken token, StaplerRequest req, StaplerResponse rsp) throws IOException { if (!Jenkins.getInstance().isUseSecurity()) return; // everyone is authorized @@ -75,7 +80,7 @@ public final class BuildAuthorizationToken { throw new AccessDeniedException(Messages.BuildAuthorizationToken_InvalidTokenProvided()); } - project.checkPermission(AbstractProject.BUILD); + project.checkPermission(Item.BUILD); if (req.getMethod().equals("POST")) { return; diff --git a/core/src/main/java/hudson/model/BuildStepListener.java b/core/src/main/java/hudson/model/BuildStepListener.java index 03f36a0f201a1a0adc20931b429a30f7ed9a4e6a..7d9ba256edc55f86e0d33249e35e6f5688844145 100644 --- a/core/src/main/java/hudson/model/BuildStepListener.java +++ b/core/src/main/java/hudson/model/BuildStepListener.java @@ -5,8 +5,6 @@ import hudson.ExtensionPoint; import hudson.tasks.BuildStep; import jenkins.model.Jenkins; -import java.util.List; - /** * Receives events that happen as a build executes {@link BuildStep}s. * @author Nicolas De Loof @@ -28,6 +26,7 @@ public abstract class BuildStepListener implements ExtensionPoint { * Returns all the registered {@link BuildStepListener}s. */ public static ExtensionList all() { + // TODO should have a null-safe version when Jenkins.getInstance() is null; would require changes in ExtensionList return Jenkins.getInstance().getExtensionList(BuildStepListener.class); } } diff --git a/core/src/main/java/hudson/model/BuildTimelineWidget.java b/core/src/main/java/hudson/model/BuildTimelineWidget.java index ca1e226893dec7f421129ef420f92bf791ddb46c..9b8bc849b860100e957e392b5aa4e094ef8d3716 100644 --- a/core/src/main/java/hudson/model/BuildTimelineWidget.java +++ b/core/src/main/java/hudson/model/BuildTimelineWidget.java @@ -33,7 +33,7 @@ import java.io.IOException; import java.util.Date; /** - * UI widget for showing the SMILE timeline control. + * UI widget for showing the SIMILE timeline control. * *

* Return this from your "getTimeline" method. @@ -45,13 +45,15 @@ public class BuildTimelineWidget { protected final RunList builds; public BuildTimelineWidget(RunList builds) { - this.builds = builds; + this.builds = builds.limit(20); // TODO instead render lazily } + @Deprecated public Run getFirstBuild() { return builds.getFirstBuild(); } + @Deprecated public Run getLastBuild() { return builds.getLastBuild(); } diff --git a/core/src/main/java/hudson/model/Cause.java b/core/src/main/java/hudson/model/Cause.java index 187bed2ce44bf1a48f000706a29b92e3f7d1c229..e07b4107c70bd856577cc297d41c318df8a6a57f 100644 --- a/core/src/main/java/hudson/model/Cause.java +++ b/core/src/main/java/hudson/model/Cause.java @@ -34,6 +34,8 @@ import jenkins.model.Jenkins; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import com.thoughtworks.xstream.converters.UnmarshallingContext; +import hudson.Util; +import java.io.IOException; import java.util.HashSet; import java.util.Set; import javax.annotation.CheckForNull; @@ -68,13 +70,40 @@ public abstract class Cause { public abstract String getShortDescription(); /** - * Called when the cause is registered to {@link AbstractBuild}. - * - * @param build - * never null - * @since 1.376 + * Called when the cause is registered. + * @since 1.568 + */ + public void onAddedTo(@Nonnull Run build) { + if (build instanceof AbstractBuild) { + onAddedTo((AbstractBuild) build); + } + } + + @Deprecated + public void onAddedTo(AbstractBuild build) { + if (Util.isOverridden(Cause.class, getClass(), "onAddedTo", Run.class)) { + onAddedTo((Run) build); + } + } + + /** + * Called when a build is loaded from disk. + * Useful in case the cause needs to keep a build reference; + * this ought to be {@code transient}. + * @since 1.568 */ - public void onAddedTo(AbstractBuild build) {} + public void onLoad(@Nonnull Run build) { + if (build instanceof AbstractBuild) { + onLoad((AbstractBuild) build); + } + } + + @Deprecated + public void onLoad(AbstractBuild build) { + if (Util.isOverridden(Cause.class, getClass(), "onLoad", Run.class)) { + onLoad((Run) build); + } + } /** * Report a line to the listener about this cause. @@ -318,8 +347,7 @@ public abstract class Cause { @Exported(visibility=3) public String getUserName() { - User u = User.get(authenticationName, false); - return u != null ? u.getDisplayName() : authenticationName; + return User.get(authenticationName).getDisplayName(); } @Override @@ -360,13 +388,7 @@ public abstract class Cause { @Exported(visibility = 3) public String getUserName() { - String userName = "anonymous"; - if (userId != null) { - User user = User.get(userId, false); - if (user != null) - userName = user.getDisplayName(); - } - return userName; + return userId == null ? "anonymous" : User.get(userId).getDisplayName(); } @Override @@ -377,6 +399,7 @@ public abstract class Cause { @Override public void print(TaskListener listener) { listener.getLogger().println(Messages.Cause_UserIdCause_ShortDescription( + // TODO better to use ModelHyperlinkNote.encodeTo(User), or User.getUrl, since it handles URL escaping ModelHyperlinkNote.encodeTo("/user/"+getUserId(), getUserName()))); } @@ -404,10 +427,13 @@ public abstract class Cause { @Override public String getShortDescription() { if(note != null) { - return Messages.Cause_RemoteCause_ShortDescriptionWithNote(addr, note); - } else { - return Messages.Cause_RemoteCause_ShortDescription(addr); + try { + return Messages.Cause_RemoteCause_ShortDescriptionWithNote(addr, Jenkins.getInstance().getMarkupFormatter().translate(note)); + } catch (IOException x) { + // ignore + } } + return Messages.Cause_RemoteCause_ShortDescription(addr); } @Override diff --git a/core/src/main/java/hudson/model/CauseAction.java b/core/src/main/java/hudson/model/CauseAction.java index 38b5f6284a2d495e46435f7dcdfb3b2b184d1236..afe715cef091f7832b987854a8d91cdadefe91ea 100644 --- a/core/src/main/java/hudson/model/CauseAction.java +++ b/core/src/main/java/hudson/model/CauseAction.java @@ -101,8 +101,10 @@ public class CauseAction implements FoldableAction, RunAction2 { public Map getCauseCounts() { Map result = new LinkedHashMap(); for (Cause c : causes) { - Integer i = result.get(c); - result.put(c, i == null ? 1 : i.intValue() + 1); + if (c != null) { + Integer i = result.get(c); + result.put(c, i == null ? 1 : i.intValue() + 1); + } } return result; } @@ -116,18 +118,21 @@ public class CauseAction implements FoldableAction, RunAction2 { return causes.get(0).getShortDescription(); } - @Override public void onLoad(Run r) { - // noop + @Override public void onLoad(Run owner) { + for (Cause c : causes) { + if (c != null) { + c.onLoad(owner); + } + } } /** * When hooked up to build, notify {@link Cause}s. */ @Override public void onAttached(Run owner) { - if (owner instanceof AbstractBuild) {// this should be always true but being defensive here - AbstractBuild b = (AbstractBuild) owner; - for (Cause c : causes) { - c.onAddedTo(b); + for (Cause c : causes) { + if (c != null) { + c.onAddedTo(owner); } } } @@ -139,7 +144,7 @@ public class CauseAction implements FoldableAction, RunAction2 { return; } // no CauseAction found, so add a copy of this one - item.getActions().add(new CauseAction(this)); + item.addAction(new CauseAction(this)); } public static class ConverterImpl extends XStream2.PassthruConverter { diff --git a/core/src/main/java/hudson/model/ChoiceParameterDefinition.java b/core/src/main/java/hudson/model/ChoiceParameterDefinition.java old mode 100755 new mode 100644 index 3b983ed0d931f13aeb45172966e23040159b8108..4da0c2998510ac9ffc96c09953a2ad45daf4ff0c --- a/core/src/main/java/hudson/model/ChoiceParameterDefinition.java +++ b/core/src/main/java/hudson/model/ChoiceParameterDefinition.java @@ -1,5 +1,7 @@ package hudson.model; +import hudson.util.FormValidation; +import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.export.Exported; @@ -15,25 +17,26 @@ import java.util.Arrays; * @author huybrechts */ public class ChoiceParameterDefinition extends SimpleParameterDefinition { + public static final String CHOICES_DELIMETER = "\\r?\\n"; + private final List choices; private final String defaultValue; + public static boolean areValidChoices(String choices) { + String strippedChoices = choices.trim(); + return !StringUtils.isEmpty(strippedChoices) && strippedChoices.split(CHOICES_DELIMETER).length > 0; + } + @DataBoundConstructor public ChoiceParameterDefinition(String name, String choices, String description) { super(name, description); - this.choices = Arrays.asList(choices.split("\\r?\\n")); - if (choices.length()==0) { - throw new IllegalArgumentException("No choices found"); - } + this.choices = Arrays.asList(choices.split(CHOICES_DELIMETER)); defaultValue = null; } public ChoiceParameterDefinition(String name, String[] choices, String description) { super(name, description); this.choices = new ArrayList(Arrays.asList(choices)); - if (this.choices.isEmpty()) { - throw new IllegalArgumentException("No choices found"); - } defaultValue = null; } @@ -52,7 +55,7 @@ public class ChoiceParameterDefinition extends SimpleParameterDefinition { return this; } } - + @Exported public List getChoices() { return choices; @@ -95,6 +98,17 @@ public class ChoiceParameterDefinition extends SimpleParameterDefinition { public String getHelpFile() { return "/help/parameter/choice.html"; } + + /** + * Checks if parameterised build choices are valid. + */ + public FormValidation doCheckChoices(@QueryParameter String value) { + if (ChoiceParameterDefinition.areValidChoices(value)) { + return FormValidation.ok(); + } else { + return FormValidation.error(Messages.ChoiceParameterDefinition_MissingChoices()); + } + } } } \ No newline at end of file diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index b4a052349e31fed0ae5c670b7006af22ef762770..10e4e871e1a287131ad9df1f30e65096d111b22e 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -51,13 +51,16 @@ import hudson.slaves.RetentionStrategy; import hudson.slaves.WorkspaceList; import hudson.slaves.OfflineCause; import hudson.slaves.OfflineCause.ByCLI; +import hudson.util.DaemonThreadFactory; import hudson.util.EditDistance; import hudson.util.ExceptionCatchingThreadFactory; import hudson.util.RemotingDiagnostics; import hudson.util.RemotingDiagnostics.HeapDump; import hudson.util.RunList; import hudson.util.Futures; +import hudson.util.NamingThreadFactory; import jenkins.model.Jenkins; +import jenkins.util.ContextResettingExecutorService; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.stapler.StaplerRequest; @@ -85,8 +88,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.LogRecord; import java.util.logging.Level; import java.util.logging.Logger; @@ -97,6 +98,7 @@ import java.net.Inet4Address; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import static javax.servlet.http.HttpServletResponse.*; @@ -180,6 +182,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces /** * Returns the transient {@link Action}s associated with the computer. */ + @SuppressWarnings("deprecation") public List getActions() { List result = new ArrayList(); result.addAll(super.getActions()); @@ -189,9 +192,10 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces } result.addAll(transientActions); } - return result; + return Collections.unmodifiableList(result); } + @SuppressWarnings("deprecation") @Override public void addAction(Action a) { if(a==null) throw new IllegalArgumentException(); @@ -200,7 +204,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces /** * This is where the log from the remote agent goes. - * + * The method also creates a log directory if required. * @see #relocateOldLogs() */ protected File getLogFile() { @@ -452,8 +456,8 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces /** * Returns {@link Node#getNodeName() the name of the node}. */ - public String getName() { - return nodeName; + public @Nonnull String getName() { + return nodeName != null ? nodeName : ""; } /** @@ -471,7 +475,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces @Exported public LoadStatistics getLoadStatistics() { - return LabelAtom.get(nodeName != null ? nodeName : "").loadStatistics; + return LabelAtom.get(nodeName != null ? nodeName : Jenkins.getInstance().getSelfLabel().toString()).loadStatistics; } public BuildTimelineWidget getTimeline() { @@ -613,7 +617,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces } @Exported - public String getDisplayName() { + @Override public @Nonnull String getDisplayName() { return nodeName; } @@ -622,7 +626,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces } public String getUrl() { - return "computer/" + Util.rawEncode(getDisplayName()) + "/"; + return "computer/" + Util.rawEncode(getName()) + "/"; } /** @@ -718,7 +722,6 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces for (Integer number : availableNumbers) { Executor e = new Executor(this, number); - e.start(); executors.add(e); } } @@ -847,7 +850,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces */ protected boolean isAlive() { for (Executor e : executors) - if (e.isAlive()) + if (e.isActive()) return true; return false; } @@ -862,7 +865,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces } public String getSearchUrl() { - return "computer/"+nodeName; + return getUrl(); } /** @@ -915,7 +918,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces * @see ProcStarter#envs(Map) * @since 1.489 */ - public EnvVars buildEnvironment(TaskListener listener) throws IOException, InterruptedException { + public @Nonnull EnvVars buildEnvironment(@Nonnull TaskListener listener) throws IOException, InterruptedException { EnvVars env = new EnvVars(); Node node = getNode(); @@ -930,7 +933,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces } // TODO: hmm, they don't really belong - String rootUrl = Hudson.getInstance().getRootUrl(); + String rootUrl = Jenkins.getInstance().getRootUrl(); if(rootUrl!=null) { env.put("HUDSON_URL", rootUrl); // Legacy. env.put("JENKINS_URL", rootUrl); @@ -1015,8 +1018,8 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces * Starts executing a fly-weight task. */ /*package*/ final void startFlyWeightTask(WorkUnit p) { - OneOffExecutor e = new OneOffExecutor(this, p); - e.start(); + OneOffExecutor e = new OneOffExecutor(this); + e.start(p); oneOffExecutors.add(e); } @@ -1061,19 +1064,10 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces private static final long serialVersionUID = 1L; } - public static final ExecutorService threadPoolForRemoting = Executors.newCachedThreadPool(new ExceptionCatchingThreadFactory( - new ThreadFactory() { - - private final AtomicInteger threadNumber = new AtomicInteger(1); - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setName("Jenkins-Remoting-Thread-"+threadNumber.getAndIncrement()); - t.setDaemon(true); - return t; - } - })); + public static final ExecutorService threadPoolForRemoting = new ContextResettingExecutorService( + Executors.newCachedThreadPool( + new ExceptionCatchingThreadFactory( + new NamingThreadFactory(new DaemonThreadFactory(), "Computer.threadPoolForRemoting")))); // // @@ -1091,14 +1085,13 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces runs.newBuilds(), Run.FEED_ADAPTER, req, rsp ); } + @RequirePOST public HttpResponse doToggleOffline(@QueryParameter String offlineMessage) throws IOException, ServletException { if(!temporarilyOffline) { checkPermission(DISCONNECT); offlineMessage = Util.fixEmptyAndTrim(offlineMessage); setTemporarilyOffline(!temporarilyOffline, - OfflineCause.create(hudson.slaves.Messages._SlaveComputer_DisconnectedBy( - Jenkins.getAuthentication().getName(), - offlineMessage!=null ? " : " + offlineMessage : ""))); + new OfflineCause.UserCause(User.current(), offlineMessage)); } else { checkPermission(CONNECT); setTemporarilyOffline(!temporarilyOffline,null); @@ -1106,13 +1099,12 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces return HttpResponses.redirectToDot(); } + @RequirePOST public HttpResponse doChangeOfflineCause(@QueryParameter String offlineMessage) throws IOException, ServletException { checkPermission(DISCONNECT); offlineMessage = Util.fixEmptyAndTrim(offlineMessage); setTemporarilyOffline(true, - OfflineCause.create(hudson.slaves.Messages._SlaveComputer_DisconnectedBy( - Jenkins.getAuthentication().getName(), - offlineMessage != null ? " : " + offlineMessage : ""))); + new OfflineCause.UserCause(User.current(), offlineMessage)); return HttpResponses.redirectToDot(); } @@ -1204,7 +1196,11 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces // read checkPermission(EXTENDED_READ); rsp.setContentType("application/xml"); - Jenkins.XSTREAM2.toXMLUTF8(getNode(), rsp.getOutputStream()); + Node node = getNode(); + if (node == null) { + throw HttpResponses.notFound(); + } + Jenkins.XSTREAM2.toXMLUTF8(node, rsp.getOutputStream()); return; } if (req.getMethod().equals("POST")) { @@ -1226,7 +1222,8 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces // replace the old Node object by the new one synchronized (app) { List nodes = new ArrayList(app.getNodes()); - int i = nodes.indexOf(getNode()); + Node node = getNode(); + int i = (node != null) ? nodes.indexOf(node) : -1; if(i<0) { throw new IOException("This slave appears to be removed while you were editing the configuration"); } @@ -1251,6 +1248,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces * Really deletes the slave. */ @CLIMethod(name="delete-node") + @RequirePOST public HttpResponse doDoDelete() throws IOException { checkPermission(DELETE); Node node = getNode(); @@ -1355,7 +1353,12 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces if (m.matches()) { File newLocation = new File(dir, "logs/slaves/" + m.group(1) + "/slave.log" + Util.fixNull(m.group(2))); newLocation.getParentFile().mkdirs(); - f.renameTo(newLocation); + boolean relocationSuccessfull=f.renameTo(newLocation); + if (relocationSuccessfull) { // The operation will fail if mkdir fails + LOGGER.log(Level.INFO, "Relocated log file {0} to {1}",new Object[] {f.getPath(),newLocation.getPath()}); + } else { + LOGGER.log(Level.WARNING, "Cannot relocate log file {0} to {1}",new Object[] {f.getPath(),newLocation.getPath()}); + } } else { assert false; } diff --git a/core/src/main/java/hudson/model/ComputerPanelBox.java b/core/src/main/java/hudson/model/ComputerPanelBox.java index 9854650655d360e31094ef57e2dc0999c5f139c3..43c945b2f561e7ee25d75b3274ed3ad016f1e09d 100644 --- a/core/src/main/java/hudson/model/ComputerPanelBox.java +++ b/core/src/main/java/hudson/model/ComputerPanelBox.java @@ -3,6 +3,7 @@ package hudson.model; import hudson.ExtensionPoint; import java.util.ArrayList; import java.util.List; +import jenkins.model.Jenkins; /** * Adds box rendered in the computer side panel. @@ -37,7 +38,7 @@ public abstract class ComputerPanelBox implements ExtensionPoint{ */ public static List all(Computer computer) { List boxs = new ArrayList(); - for(ComputerPanelBox box: Hudson.getInstance().getExtensionList(ComputerPanelBox.class)){ + for(ComputerPanelBox box: Jenkins.getInstance().getExtensionList(ComputerPanelBox.class)){ box.setComputer(computer); boxs.add(box); } diff --git a/core/src/main/java/hudson/model/ComputerSet.java b/core/src/main/java/hudson/model/ComputerSet.java index cb9922e0b8c66b0ade73ab791f4c6d3942032e31..0502c64329059df41e606c956aa8227b3b564e6c 100644 --- a/core/src/main/java/hudson/model/ComputerSet.java +++ b/core/src/main/java/hudson/model/ComputerSet.java @@ -28,16 +28,19 @@ import hudson.DescriptorExtensionList; import hudson.Extension; import hudson.Util; import hudson.XmlFile; +import hudson.init.Initializer; import hudson.model.Descriptor.FormException; import hudson.model.listeners.SaveableListener; import hudson.node_monitors.NodeMonitor; import hudson.slaves.NodeDescriptor; +import hudson.triggers.SafeTimerTask; import hudson.util.DescribableList; import hudson.util.FormApply; import hudson.util.FormValidation; import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithChildren; import jenkins.model.ModelObjectWithContextMenu.ContextMenu; +import jenkins.util.Timer; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; @@ -47,7 +50,6 @@ import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; import javax.servlet.ServletException; -import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import java.io.File; import java.io.IOException; import java.util.AbstractList; @@ -55,10 +57,13 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.json.JSONObject; +import static hudson.init.InitMilestone.JOB_LOADED; + /** * Serves as the top of {@link Computer}s in the URL hierarchy. *

@@ -236,12 +241,11 @@ public final class ComputerSet extends AbstractModelObject implements Describabl Node src = app.getNode(from); if(src==null) { - rsp.setStatus(SC_BAD_REQUEST); - if(Util.fixEmpty(from)==null) - sendError(Messages.ComputerSet_SpecifySlaveToCopy(),req,rsp); - else - sendError(Messages.ComputerSet_NoSuchSlave(from),req,rsp); - return; + if (Util.fixEmpty(from) == null) { + throw new Failure(Messages.ComputerSet_SpecifySlaveToCopy()); + } else { + throw new Failure(Messages.ComputerSet_NoSuchSlave(from)); + } } // copy through XStream @@ -260,12 +264,14 @@ public final class ComputerSet extends AbstractModelObject implements Describabl rsp.sendRedirect2(result.getNodeName()+"/configure"); } else { // proceed to step 2 - if(mode==null) { - rsp.sendError(SC_BAD_REQUEST); - return; + if (mode == null) { + throw new Failure("No mode given"); } NodeDescriptor d = NodeDescriptor.all().findByName(mode); + if (d == null) { + throw new Failure("No node type ‘" + mode + "’ is known"); + } d.handleNewNodePage(this,name,req,rsp); } } @@ -397,6 +403,16 @@ public final class ComputerSet extends AbstractModelObject implements Describabl */ public static void initialize() {} + @Initializer(after= JOB_LOADED) + public static void init() { + // start monitoring nodes, although there's no hurry. + Timer.get().schedule(new SafeTimerTask() { + public void doRun() { + ComputerSet.initialize(); + } + }, 10, TimeUnit.SECONDS); + } + private static final Logger LOGGER = Logger.getLogger(ComputerSet.class.getName()); static { diff --git a/core/src/main/java/hudson/model/DependencyGraph.java b/core/src/main/java/hudson/model/DependencyGraph.java index 5b1ca1d11e735d19e8b34e38ac5407f37c89c0b2..bfbaa4e9997f1ae7ee1e97ee3a0b78e874a1f390 100644 --- a/core/src/main/java/hudson/model/DependencyGraph.java +++ b/core/src/main/java/hudson/model/DependencyGraph.java @@ -415,6 +415,10 @@ public class DependencyGraph implements Comparator { * Decide whether build should be triggered and provide any Actions for the build. * Default implementation always returns true (for backward compatibility), and * adds no Actions. Subclasses may override to control how/if the build is triggered. + *

The authentication in effect ({@link Jenkins#getAuthentication}) will be that of the upstream build. + * An implementation is expected to perform any relevant access control checks: + * that an upstream project can both see and build a downstream project, + * or that a downstream project can see an upstream project. * @param build Build of upstream project that just completed * @param listener For any error/log output * @param actions Add Actions for the triggered build to this list; never null diff --git a/core/src/main/java/hudson/model/Describable.java b/core/src/main/java/hudson/model/Describable.java index d916ed34889114606576d6f062dba18f498374e3..ae8103b3cfd916456ae9b2dc9e9456abb6153375 100644 --- a/core/src/main/java/hudson/model/Describable.java +++ b/core/src/main/java/hudson/model/Describable.java @@ -34,8 +34,9 @@ public interface Describable> { * *

* {@link Descriptor} is a singleton for every concrete {@link Describable} - * implementation, so if a.getClass()==b.getClass() then - * a.getDescriptor()==b.getDescriptor() must hold. + * implementation, so if {@code a.getClass() == b.getClass()} then by default + * {@code a.getDescriptor() == b.getDescriptor()} as well. + * (In rare cases a single implementation class may be used for instances with distinct descriptors.) */ Descriptor getDescriptor(); } diff --git a/core/src/main/java/hudson/model/Descriptor.java b/core/src/main/java/hudson/model/Descriptor.java index e0cc80c5ea3ba5b60b717d6a6a15a8a0290ea9fc..1d36481101073a5bf44cdfb27deba09c60f28945 100644 --- a/core/src/main/java/hudson/model/Descriptor.java +++ b/core/src/main/java/hudson/model/Descriptor.java @@ -967,7 +967,7 @@ public abstract class Descriptor> implements Saveable { public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { if (FormApply.isApply(req)) { - FormApply.applyResponse("notificationBar.show(" + quote(getMessage())+ ",notificationBar.defaultOptions.ERROR)") + FormApply.applyResponse("notificationBar.show(" + quote(getMessage())+ ",notificationBar.ERROR)") .generateResponse(req, rsp, node); } else { // for now, we can't really use the field name that caused the problem. diff --git a/core/src/main/java/hudson/model/DescriptorVisibilityFilter.java b/core/src/main/java/hudson/model/DescriptorVisibilityFilter.java index 2dc1be651993fec22df0d13314e7e1eb2679c63c..774088bc41a06c848695adb7abbffb08b40d43b1 100644 --- a/core/src/main/java/hudson/model/DescriptorVisibilityFilter.java +++ b/core/src/main/java/hudson/model/DescriptorVisibilityFilter.java @@ -9,6 +9,9 @@ import jenkins.model.Jenkins; import java.util.ArrayList; import java.util.List; +import java.util.logging.Logger; +import java.util.logging.Level; + /** * Hides {@link Descriptor}s from users. * @@ -18,6 +21,8 @@ import java.util.List; */ public abstract class DescriptorVisibilityFilter implements ExtensionPoint { + private static final Logger LOGGER = Logger.getLogger(DescriptorVisibilityFilter.class.getName()); + /** * Decides if the given descriptor should be visible to the user. * @@ -46,9 +51,19 @@ public abstract class DescriptorVisibilityFilter implements ExtensionPoint { OUTER: for (T d : source) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("Determining visibility of " + d + " in context " + context); + } for (DescriptorVisibilityFilter f : filters) { - if (!f.filter(context,d)) + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.finer("Querying " + f + " for visibility of " + d + " in " + context); + } + if (!f.filter(context,d)) { + if (LOGGER.isLoggable(Level.CONFIG)) { + LOGGER.config("Filter " + f + " hides " + d + " in context " + context); + } continue OUTER; // veto-ed. not shown + } } r.add(d); } diff --git a/core/src/main/java/hudson/model/DirectlyModifiableView.java b/core/src/main/java/hudson/model/DirectlyModifiableView.java new file mode 100644 index 0000000000000000000000000000000000000000..bdc651341b59219520c6ebe8baaa277aab34aa52 --- /dev/null +++ b/core/src/main/java/hudson/model/DirectlyModifiableView.java @@ -0,0 +1,79 @@ +/* + * The MIT License + * + * Copyright (c) 2014 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.model; + +import hudson.util.HttpResponses; + +import java.io.IOException; + +import javax.annotation.Nonnull; +import javax.servlet.ServletException; + +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; + +/** + * Marker interface for {@link View} its items can be modified. + * + * @author ogondza + * @since 1.566 + */ +public interface DirectlyModifiableView { + + /** + * Remove item from this view. + * + * @return false if item not present in view, true if removed. + * @throws IOException Removal failed. + * @throws IllegalArgumentException View rejected to remove an item. + */ + boolean remove(@Nonnull TopLevelItem item) throws IOException, IllegalArgumentException; + + /** + * Add item to this view. + * + * @throws IOException Adding failed. + * @throws IllegalArgumentException View rejected to add an item. + */ + void add(@Nonnull TopLevelItem item) throws IOException, IllegalArgumentException; + + /** + * Handle addJobToView web method. + * + * This method should {@link RequirePOST}. + * + * @param name Item name. This can be either full name relative to owner item group or full item name prefixed with '/'. + */ + HttpResponse doAddJobToView(@QueryParameter String name) throws IOException, ServletException; + + /** + * Handle removeJobFromView web method. + * + * This method should {@link RequirePOST}. + * + * @param name Item name. This can be either full name relative to owner item group or full item name prefixed with '/'. + */ + HttpResponse doRemoveJobFromView(@QueryParameter String name) throws IOException, ServletException; +} diff --git a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java index 09e07bed2dd30422f54ce0814dc03ebbae80a80f..84be4be622cd6f1e19e333b707e6932adc53b560 100644 --- a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java +++ b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java @@ -25,14 +25,7 @@ package hudson.model; import hudson.FilePath; import hudson.Util; -import hudson.util.IOException2; -import jenkins.model.Jenkins; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.HttpResponse; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletResponse; +import hudson.remoting.Callable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -45,11 +38,18 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.StringTokenizer; -import java.util.logging.Logger; import java.util.logging.Level; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; +import java.util.logging.Logger; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import jenkins.model.Jenkins; import jenkins.util.VirtualFile; +import org.apache.commons.io.IOUtils; +import org.apache.tools.zip.ZipEntry; +import org.apache.tools.zip.ZipOutputStream; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; /** * Has convenience methods to serve file system. @@ -122,7 +122,7 @@ public final class DirectoryBrowserSupport implements HttpResponse { try { serveFile(req,rsp,base,icon,serveDirIndex); } catch (InterruptedException e) { - throw new IOException2("interrupted",e); + throw new IOException("interrupted",e); } } @@ -251,7 +251,7 @@ public final class DirectoryBrowserSupport implements HttpResponse { } else if(serveDirIndex) { // serve directory index - glob = buildChildPaths(baseFile, req.getLocale()); + glob = baseFile.run(new BuildChildPaths(baseFile, req.getLocale())); } if(glob!=null) { @@ -340,12 +340,25 @@ public final class DirectoryBrowserSupport implements HttpResponse { private static void zip(OutputStream outputStream, VirtualFile dir, String glob) throws IOException { ZipOutputStream zos = new ZipOutputStream(outputStream); + zos.setEncoding(System.getProperty("file.encoding")); // TODO JENKINS-20663 make this overridable via query parameter for (String n : dir.list(glob.length() == 0 ? "**" : glob)) { - ZipEntry e = new ZipEntry(n); + String relativePath; + if (glob.length() == 0) { + // JENKINS-19947: traditional behavior is to prepend the directory name + relativePath = dir.getName() + '/' + n; + } else { + relativePath = n; + } + ZipEntry e = new ZipEntry(relativePath); VirtualFile f = dir.child(n); e.setTime(f.lastModified()); zos.putNextEntry(e); - Util.copyStream(f.open(), zos); + InputStream in = f.open(); + try { + Util.copyStream(in, zos); + } finally { + IOUtils.closeQuietly(in); + } zos.closeEntry(); } zos.close(); @@ -419,7 +432,7 @@ public final class DirectoryBrowserSupport implements HttpResponse { private static final class FileComparator implements Comparator { private Collator collator; - public FileComparator(Locale locale) { + FileComparator(Locale locale) { this.collator = Collator.getInstance(locale); } @@ -441,6 +454,17 @@ public final class DirectoryBrowserSupport implements HttpResponse { } } + private static final class BuildChildPaths implements Callable>,IOException> { + private final VirtualFile cur; + private final Locale locale; + BuildChildPaths(VirtualFile cur, Locale locale) { + this.cur = cur; + this.locale = locale; + } + @Override public List> call() throws IOException { + return buildChildPaths(cur, locale); + } + } /** * Builds a list of list of {@link Path}. The inner * list of {@link Path} represents one child item to be shown diff --git a/core/src/main/java/hudson/model/DownloadService.java b/core/src/main/java/hudson/model/DownloadService.java index 4ca6c5b616b43a4e9e33e57e7eae96b3fafcae43..ddb9548e93cd6b7f3ccc13185a617a9ae64e383a 100644 --- a/core/src/main/java/hudson/model/DownloadService.java +++ b/core/src/main/java/hudson/model/DownloadService.java @@ -26,27 +26,30 @@ package hudson.model; import hudson.Extension; import hudson.ExtensionList; import hudson.ExtensionPoint; +import hudson.ProxyConfiguration; import hudson.util.FormValidation; import hudson.util.FormValidation.Kind; -import hudson.util.IOException2; -import hudson.util.IOUtils; import hudson.util.QuotedStringTokenizer; import hudson.util.TextFile; -import jenkins.model.Jenkins; -import jenkins.util.JSONSignatureValidator; -import net.sf.json.JSONException; -import org.kohsuke.stapler.Stapler; - +import static hudson.util.TimeUnit2.DAYS; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLEncoder; import java.util.logging.Logger; - +import jenkins.model.DownloadSettings; +import jenkins.model.Jenkins; +import jenkins.util.JSONSignatureValidator; +import net.sf.json.JSONException; import net.sf.json.JSONObject; +import org.apache.commons.io.IOUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; -import static hudson.util.TimeUnit2.DAYS; - /** * Service for plugins to periodically retrieve update data files * (like the one in the update center) through browsers. @@ -63,6 +66,9 @@ public class DownloadService extends PageDecorator { * Builds up an HTML fragment that starts all the download jobs. */ public String generateFragment() { + if (!DownloadSettings.get().isUseBrowser()) { + return ""; + } if (neverUpdate) return ""; if (doesNotSupportPostMessage()) return ""; @@ -136,6 +142,34 @@ public class DownloadService extends PageDecorator { return null; } + /** + * Loads JSON from a JSONP URL. + * Metadata for downloadables and update centers is offered in two formats, both designed for download from the browser (predating {@link DownloadSettings}): + * HTML using {@code postMessage} for newer browsers, and JSONP as a fallback. + * Confusingly, the JSONP files are given the {@code *.json} file extension, when they are really JavaScript and should be {@code *.js}. + * This method extracts the JSON from a JSONP URL, since that is what we actually want when we download from the server. + * (Currently the true JSON is not published separately, and extracting from the {@code *.json.html} is more work.) + * @param src a URL to a JSONP file (typically including {@code id} and {@code version} query parameters) + * @return the embedded JSON text + * @throws IOException if either downloading or processing failed + */ + @Restricted(NoExternalUse.class) + public static String loadJSON(URL src) throws IOException { + InputStream is = ProxyConfiguration.open(src).getInputStream(); + try { + String jsonp = IOUtils.toString(is, "UTF-8"); + int start = jsonp.indexOf('{'); + int end = jsonp.lastIndexOf('}'); + if (start >= 0 && end > start) { + return jsonp.substring(start, end + 1); + } else { + throw new IOException("Could not find JSON in " + src); + } + } finally { + is.close(); + } + } + /** * Represents a periodically updated JSON data file obtained from a remote URL. * @@ -240,7 +274,7 @@ public class DownloadService extends PageDecorator { return JSONObject.fromObject(df.read()); } catch (JSONException e) { df.delete(); // if we keep this file, it will cause repeated failures - throw new IOException2("Failed to parse "+df+" into JSON",e); + throw new IOException("Failed to parse "+df+" into JSON",e); } return null; } @@ -249,17 +283,28 @@ public class DownloadService extends PageDecorator { * This is where the browser sends us the data. */ public void doPostBack(StaplerRequest req, StaplerResponse rsp) throws IOException { + if (!DownloadSettings.get().isUseBrowser()) { + throw new IOException("not allowed"); + } long dataTimestamp = System.currentTimeMillis(); due = dataTimestamp+getInterval(); // success or fail, don't try too often String json = IOUtils.toString(req.getInputStream(),"UTF-8"); + FormValidation e = load(json, dataTimestamp); + if (e.kind != Kind.OK) { + LOGGER.severe(e.renderHtml()); + throw e; + } + rsp.setContentType("text/plain"); // So browser won't try to parse response + } + + private FormValidation load(String json, long dataTimestamp) throws IOException { JSONObject o = JSONObject.fromObject(json); if (signatureCheck) { FormValidation e = new JSONSignatureValidator("downloadable '"+id+"'").verifySignature(o); if (e.kind!= Kind.OK) { - LOGGER.severe(e.renderHtml()); - throw e; + return e; } } @@ -267,8 +312,12 @@ public class DownloadService extends PageDecorator { df.write(json); df.file.setLastModified(dataTimestamp); LOGGER.info("Obtained the updated data file for "+id); + return FormValidation.ok(); + } - rsp.setContentType("text/plain"); // So browser won't try to parse response + @Restricted(NoExternalUse.class) + public FormValidation updateNow() throws IOException { + return load(loadJSON(new URL(getUrl() + "?id=" + URLEncoder.encode(getId(), "UTF-8") + "&version=" + URLEncoder.encode(Jenkins.VERSION, "UTF-8"))), System.currentTimeMillis()); } /** @@ -297,7 +346,10 @@ public class DownloadService extends PageDecorator { public static boolean neverUpdate = Boolean.getBoolean(DownloadService.class.getName()+".never"); /** - * Off by default until we know this is reasonably working. + * May be used to temporarily disable signature checking on {@link DownloadService} and {@link UpdateCenter}. + * Useful when upstream signatures are broken, such as due to expired certificates. + * Should only be used when {@link DownloadSettings#isUseBrowser}; + * disabling signature checks for in-browser downloads is very dangerous as unprivileged users could submit spoofed metadata! */ public static boolean signatureCheck = !Boolean.getBoolean(DownloadService.class.getName()+".noSignatureCheck"); } diff --git a/core/src/main/java/hudson/model/EnvironmentContributor.java b/core/src/main/java/hudson/model/EnvironmentContributor.java index 37084b10cbcfa40c203f048aab69d63e86b84a63..cbe05353d70ed5920a9f0edeb9bb72cf8f134b97 100644 --- a/core/src/main/java/hudson/model/EnvironmentContributor.java +++ b/core/src/main/java/hudson/model/EnvironmentContributor.java @@ -31,6 +31,7 @@ import hudson.scm.SCM; import jenkins.model.Jenkins; import java.io.IOException; +import javax.annotation.Nonnull; /** * Contributes environment variables to builds. @@ -79,14 +80,14 @@ public abstract class EnvironmentContributor implements ExtensionPoint { * variables that are scoped to builds. * * @param r - * Build that's being performed. Never null. + * Build that's being performed. * @param envs * Partially built environment variable map. Implementation of this method is expected to - * add additional variables here. Never null. + * add additional variables here. * @param listener - * Connected to the build console. Can be used to report errors. Never null. + * Connected to the build console. Can be used to report errors. */ - public void buildEnvironmentFor(Run r, EnvVars envs, TaskListener listener) throws IOException, InterruptedException {} + public void buildEnvironmentFor(@Nonnull Run r, @Nonnull EnvVars envs, @Nonnull TaskListener listener) throws IOException, InterruptedException {} /** * Contributes environment variables used for a job. @@ -103,12 +104,12 @@ public abstract class EnvironmentContributor implements ExtensionPoint { * Job for which some activities are launched. * @param envs * Partially built environment variable map. Implementation of this method is expected to - * add additional variables here. Never null. + * add additional variables here. * @param listener - * Connected to the build console. Can be used to report errors. Never null. + * Connected to the build console. Can be used to report errors. * @since 1.527 */ - public void buildEnvironmentFor(Job j, EnvVars envs, TaskListener listener) throws IOException, InterruptedException {} + public void buildEnvironmentFor(@Nonnull Job j, @Nonnull EnvVars envs, @Nonnull TaskListener listener) throws IOException, InterruptedException {} /** * Returns all the registered {@link EnvironmentContributor}s. diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java index bb96893510eb2c163b4477eb5c8a2217dd27b2b6..4353ad754626c0bd7759f8dcf3160d2d8a5b5d18 100644 --- a/core/src/main/java/hudson/model/Executor.java +++ b/core/src/main/java/hudson/model/Executor.java @@ -23,60 +23,62 @@ */ package hudson.model; -import hudson.model.Queue.Executable; -import hudson.Util; import hudson.FilePath; -import jenkins.model.CauseOfInterruption; -import jenkins.model.CauseOfInterruption.UserInterruption; +import hudson.Util; +import hudson.model.Queue.Executable; import hudson.model.queue.Executables; import hudson.model.queue.SubTask; import hudson.model.queue.Tasks; import hudson.model.queue.WorkUnit; -import hudson.util.TimeUnit2; -import hudson.util.InterceptingProxy; import hudson.security.ACL; +import hudson.util.InterceptingProxy; +import hudson.util.TimeUnit2; +import jenkins.model.CauseOfInterruption; +import jenkins.model.CauseOfInterruption.UserInterruption; import jenkins.model.InterruptedBuildAction; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; -import org.acegisecurity.context.SecurityContext; -import org.acegisecurity.context.SecurityContextHolder; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; -import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.export.Exported; +import org.kohsuke.stapler.export.ExportedBean; +import org.kohsuke.stapler.interceptor.RequirePOST; import javax.servlet.ServletException; import java.io.IOException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Vector; -import java.util.logging.Logger; import java.util.logging.Level; -import java.lang.reflect.Method; +import java.util.logging.Logger; import static hudson.model.queue.Executables.*; -import static java.util.logging.Level.FINE; -import org.kohsuke.stapler.interceptor.RequirePOST; +import static java.util.logging.Level.*; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; /** * Thread that executes builds. - * + * Since 1.536, {@link Executor}s start threads on-demand. + * The entire logic should use {@link #isActive()} instead of {@link #isAlive()} + * in order to check if the {@link Executor} it ready to take tasks. * @author Kohsuke Kawaguchi */ @ExportedBean public class Executor extends Thread implements ModelObject { - protected final Computer owner; + protected final @Nonnull Computer owner; private final Queue queue; private long startTime; /** * Used to track when a job was last executed. */ - private long finishTime; + private final long creationTime = System.currentTimeMillis(); /** * Executor number that identifies it among other executors for the same {@link Computer}. @@ -87,12 +89,18 @@ public class Executor extends Thread implements ModelObject { */ private volatile Queue.Executable executable; + /** + * When {@link Queue} allocates a work for this executor, this field is set + * and the executor is {@linkplain Thread#start() started}. + */ private volatile WorkUnit workUnit; private Throwable causeOfDeath; private boolean induceDeath; + private volatile boolean started; + /** * When the executor is interrupted, we allow the code that interrupted the thread to override the * result code it prefers. @@ -104,7 +112,7 @@ public class Executor extends Thread implements ModelObject { */ private final List causes = new Vector(); - public Executor(Computer owner, int n) { + public Executor(@Nonnull Computer owner, int n) { super("Executor #"+n+" for "+owner.getDisplayName()); this.owner = owner; this.queue = Jenkins.getInstance().getQueue(); @@ -140,6 +148,14 @@ public class Executor extends Thread implements ModelObject { if (LOGGER.isLoggable(FINE)) LOGGER.log(FINE, String.format("%s is interrupted(%s): %s", getDisplayName(), result, Util.join(Arrays.asList(causes),",")), new InterruptedException()); + synchronized (this) { + if (!started) { + // not yet started, so simply dispose this + owner.removeExecutor(this); + return; + } + } + interruptStatus = result; synchronized (this.causes) { for (CauseOfInterruption c : causes) { @@ -178,103 +194,78 @@ public class Executor extends Thread implements ModelObject { @Override public void run() { + startTime = System.currentTimeMillis(); + // run as the system user. see ACL.SYSTEM for more discussion about why this is somewhat broken ACL.impersonate(ACL.SYSTEM); try { - finishTime = System.currentTimeMillis(); - while(shouldRun()) { - executable = null; - workUnit = null; - interruptStatus = null; - causes.clear(); - - synchronized(owner) { - if(owner.getNumExecutors() IMPERSONATION = new ThreadLocal(); private static final Logger LOGGER = Logger.getLogger(Executor.class.getName()); - } diff --git a/core/src/main/java/hudson/model/FileParameterValue.java b/core/src/main/java/hudson/model/FileParameterValue.java index e5bfda9881329b83290debe7ecea5aecdef44a30..090a50aa9f14356c6582610c98e6de0f82c9f649 100644 --- a/core/src/main/java/hudson/model/FileParameterValue.java +++ b/core/src/main/java/hudson/model/FileParameterValue.java @@ -96,11 +96,16 @@ public class FileParameterValue extends ParameterValue { return location; } + @Override + public Object getValue() { + return file; + } + /** * Exposes the originalFileName as an environment variable. */ @Override - public void buildEnvVars(AbstractBuild build, EnvVars env) { + public void buildEnvironment(Run build, EnvVars env) { env.put(name,originalFileName); } @@ -197,12 +202,16 @@ public class FileParameterValue extends ParameterValue { File fileParameter = getLocationUnderBuild(build); if (fileParameter.isFile()) { InputStream data = new FileInputStream(fileParameter); - long lastModified = fileParameter.lastModified(); - long contentLength = fileParameter.length(); - if (request.hasParameter("view")) { - response.serveFile(request, data, lastModified, contentLength, "plain.txt"); - } else { - response.serveFile(request, data, lastModified, contentLength, originalFileName); + try { + long lastModified = fileParameter.lastModified(); + long contentLength = fileParameter.length(); + if (request.hasParameter("view")) { + response.serveFile(request, data, lastModified, contentLength, "plain.txt"); + } else { + response.serveFile(request, data, lastModified, contentLength, originalFileName); + } + } finally { + IOUtils.closeQuietly(data); } } } @@ -253,7 +262,12 @@ public class FileParameterValue extends ParameterValue { public byte[] get() { try { - return IOUtils.toByteArray(new FileInputStream(file)); + FileInputStream inputStream = new FileInputStream(file); + try { + return IOUtils.toByteArray(inputStream); + } finally { + inputStream.close(); + } } catch (IOException e) { throw new Error(e); } @@ -289,6 +303,7 @@ public class FileParameterValue extends ParameterValue { public void setFormField(boolean state) { } + @Deprecated public OutputStream getOutputStream() throws IOException { return new FileOutputStream(file); } diff --git a/core/src/main/java/hudson/model/Fingerprint.java b/core/src/main/java/hudson/model/Fingerprint.java index 0e91bf7cf2ec76fcf634c3bf47799ba1a151013a..9b792a8ee6a08960535bf853d87507cd6d48ad2b 100644 --- a/core/src/main/java/hudson/model/Fingerprint.java +++ b/core/src/main/java/hudson/model/Fingerprint.java @@ -727,9 +727,9 @@ public class Fingerprint implements ModelObject, Saveable { @Extension public static final class ProjectRenameListener extends ItemListener { @Override - public void onRenamed(Item item, String oldName, String newName) { + public void onLocationChanged(Item item, String oldName, String newName) { if (item instanceof AbstractProject) { - AbstractProject p = Hudson.getInstance().getItemByFullName(newName, AbstractProject.class); + AbstractProject p = Jenkins.getInstance().getItemByFullName(newName, AbstractProject.class); if (p != null) { RunList builds = p.getBuilds(); for (Object build : builds) { @@ -1075,6 +1075,17 @@ public class Fingerprint implements ModelObject, Saveable { return r; } + /** + * Finds a facet of the specific type (including subtypes.) + */ + public T getFacet(Class type) { + for (FingerprintFacet f : getFacets()) { + if (type.isInstance(f)) + return type.cast(f); + } + return null; + } + /** * Returns the actions contributed from {@link #getFacets()} */ diff --git a/core/src/main/java/hudson/model/FingerprintCleanupThread.java b/core/src/main/java/hudson/model/FingerprintCleanupThread.java index c04830911d751933cf3dc2a35a828dedf8c47063..527dc34bc78f6db8946a0489d51cf77ebf9350de 100644 --- a/core/src/main/java/hudson/model/FingerprintCleanupThread.java +++ b/core/src/main/java/hudson/model/FingerprintCleanupThread.java @@ -29,7 +29,6 @@ import jenkins.model.Jenkins; import java.io.File; import java.io.FileFilter; import java.io.IOException; -import java.util.logging.Level; import java.util.regex.Pattern; /** @@ -72,7 +71,7 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork { for(File file2 : files2) { File[] files3 = file2.listFiles(FINGERPRINTFILE_FILTER); for(File file3 : files3) { - if(check(file3)) + if(check(file3, listener)) numFiles++; } deleteIfEmpty(file2); @@ -81,7 +80,7 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork { } } - logger.log(Level.INFO, "Cleaned up "+numFiles+" records"); + listener.getLogger().println("Cleaned up "+numFiles+" records"); } /** @@ -97,22 +96,22 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork { /** * Examines the file and returns true if a file was deleted. */ - private boolean check(File fingerprintFile) { + private boolean check(File fingerprintFile, TaskListener listener) { try { Fingerprint fp = Fingerprint.load(fingerprintFile); if (fp == null || !fp.isAlive()) { - logger.fine("deleting obsolete " + fingerprintFile); + listener.getLogger().println("deleting obsolete " + fingerprintFile); fingerprintFile.delete(); return true; } else { // get the fingerprint in the official map so have the changes visible to Jenkins // otherwise the mutation made in FingerprintMap can override our trimming. - logger.finer("possibly trimming " + fingerprintFile); + listener.getLogger().println("possibly trimming " + fingerprintFile); fp = Jenkins.getInstance()._getFingerprint(fp.getHashString()); return fp.trim(); } } catch (IOException e) { - logger.log(Level.WARNING, "Failed to process "+fingerprintFile, e); + e.printStackTrace(listener.error("Failed to process " + fingerprintFile)); return false; } } diff --git a/core/src/main/java/hudson/model/FreeStyleBuild.java b/core/src/main/java/hudson/model/FreeStyleBuild.java index df091cd142b80d0cddf4e71b10560658dc64ba2a..4941789ef279e5bed80e5c656f73218c2fa91cdd 100644 --- a/core/src/main/java/hudson/model/FreeStyleBuild.java +++ b/core/src/main/java/hudson/model/FreeStyleBuild.java @@ -23,9 +23,6 @@ */ package hudson.model; -import hudson.slaves.WorkspaceList; -import hudson.slaves.WorkspaceList.Lease; - import java.io.IOException; import java.io.File; diff --git a/core/src/main/java/hudson/model/Hudson.java b/core/src/main/java/hudson/model/Hudson.java index aff8df4341c0faae5b7b239e14b06476c43e958b..a04abb39252be453cc275610ec601a18b66d5c7c 100644 --- a/core/src/main/java/hudson/model/Hudson.java +++ b/core/src/main/java/hudson/model/Hudson.java @@ -67,7 +67,8 @@ public class Hudson extends Jenkins { */ private transient final CopyOnWriteList computerListeners = ExtensionListView.createCopyOnWriteList(ComputerListener.class); - + /** @deprecated Here only for compatibility. Use {@link Jenkins#getInstance} instead. */ + @Deprecated @CLIResolver public static Hudson getInstance() { return (Hudson)Jenkins.getInstance(); diff --git a/core/src/main/java/hudson/model/Item.java b/core/src/main/java/hudson/model/Item.java index b9092facfa2920a2637b678479c4ed0e4afeab4c..635a7547a48de4f0c7b1e508537e3d7c9be02c7a 100644 --- a/core/src/main/java/hudson/model/Item.java +++ b/core/src/main/java/hudson/model/Item.java @@ -180,7 +180,7 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont /** * Called right after when a {@link Item} is loaded from disk. - * This is an opporunity to do a post load processing. + * This is an opportunity to do a post load processing. * * @param name * Name of the directory (not a path --- just the name portion) from diff --git a/core/src/main/java/hudson/model/ItemGroup.java b/core/src/main/java/hudson/model/ItemGroup.java index a87814eb9d8a8423e64c0c8401958084c48f2bfb..bc042c8925fe27963d1e07691a4365f8d431364c 100644 --- a/core/src/main/java/hudson/model/ItemGroup.java +++ b/core/src/main/java/hudson/model/ItemGroup.java @@ -23,6 +23,7 @@ */ package hudson.model; +import hudson.model.listeners.ItemListener; import java.io.IOException; import java.util.Collection; import java.io.File; @@ -76,6 +77,7 @@ public interface ItemGroup extends PersistenceRoot, ModelObject /** * Internal method. Called by {@link Item}s when they are renamed by users. + * This is not expected to call {@link ItemListener#onRenamed}, inconsistent with {@link #onDeleted}. */ void onRenamed(T item, String oldName, String newName) throws IOException; diff --git a/core/src/main/java/hudson/model/ItemGroupMixIn.java b/core/src/main/java/hudson/model/ItemGroupMixIn.java index 2b23b27c16dd0cfaa9b29e08a86ae1a87126687f..131738ed18f9b357612a3997e15ac1c86568a9cb 100644 --- a/core/src/main/java/hudson/model/ItemGroupMixIn.java +++ b/core/src/main/java/hudson/model/ItemGroupMixIn.java @@ -24,7 +24,9 @@ package hudson.model; import hudson.Util; +import hudson.XmlFile; import hudson.model.listeners.ItemListener; +import hudson.remoting.Callable; import hudson.security.AccessControlled; import hudson.util.CopyOnWriteMap; import hudson.util.Function1; @@ -40,6 +42,8 @@ import java.io.FileFilter; import java.io.IOException; import java.io.InputStream; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Defines a bunch of static methods to be used as a "mix-in" for {@link ItemGroup} @@ -96,10 +100,22 @@ public abstract class ItemGroupMixIn { CopyOnWriteMap.Tree configurations = new CopyOnWriteMap.Tree(); for (File subdir : subdirs) { try { - V item = (V) Items.load(parent,subdir); + // Try to retain the identity of an existing child object if we can. + V item = (V) parent.getItem(subdir.getName()); + if (item == null) { + XmlFile xmlFile = Items.getConfigFile( subdir ); + if (xmlFile.exists()) { + item = (V) Items.load( parent, subdir ); + }else{ + Logger.getLogger( ItemGroupMixIn.class.getName() ).log( Level.WARNING, "could not find file " + xmlFile.getFile()); + continue; + } + } else { + item.onLoad(parent, subdir.getName()); + } configurations.put(key.call(item), item); } catch (IOException e) { - e.printStackTrace(); // TODO: logging + Logger.getLogger(ItemGroupMixIn.class.getName()).log(Level.WARNING, "could not load " + subdir, e); } } @@ -120,7 +136,7 @@ public abstract class ItemGroupMixIn { * or throws an exception if it fails. */ public synchronized TopLevelItem createTopLevelItem( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { - acl.checkPermission(Job.CREATE); + acl.checkPermission(Item.CREATE); TopLevelItem result; @@ -170,9 +186,13 @@ public abstract class ItemGroupMixIn { } else { if(mode==null) throw new Failure("No mode given"); + TopLevelItemDescriptor descriptor = Items.all().findByName(mode); + if (descriptor == null) { + throw new Failure("No item type ‘" + mode + "’ is known"); + } // create empty job and redirect to the project config screen - result = createProject(Items.all().findByName(mode), name, true); + result = createProject(descriptor, name, true); } } @@ -192,7 +212,8 @@ public abstract class ItemGroupMixIn { */ @SuppressWarnings({"unchecked"}) public synchronized T copy(T src, String name) throws IOException { - acl.checkPermission(Job.CREATE); + acl.checkPermission(Item.CREATE); + src.checkPermission(Item.EXTENDED_READ); T result = (T)createProject(src.getDescriptor(),name,false); @@ -200,12 +221,12 @@ public abstract class ItemGroupMixIn { Util.copyFile(Items.getConfigFile(src).getFile(),Items.getConfigFile(result).getFile()); // reload from the new config - Items.updatingByXml.set(true); - try { - result = (T)Items.load(parent,result.getRootDir()); - } finally { - Items.updatingByXml.set(false); - } + final File rootDir = result.getRootDir(); + result = Items.whileUpdatingByXml(new Callable() { + @Override public T call() throws IOException { + return (T) Items.load(parent, rootDir); + } + }); result.onCopiedFrom(src); add(result); @@ -216,23 +237,27 @@ public abstract class ItemGroupMixIn { } public synchronized TopLevelItem createProjectFromXML(String name, InputStream xml) throws IOException { - acl.checkPermission(Job.CREATE); + acl.checkPermission(Item.CREATE); Jenkins.getInstance().getProjectNamingStrategy().checkName(name); + if (parent.getItem(name) != null) { + throw new IllegalArgumentException(parent.getDisplayName() + " already contains an item '" + name + "'"); + } + // TODO what if we have no DISCOVER permission on the existing job? + // place it as config.xml File configXml = Items.getConfigFile(getRootDirFor(name)).getFile(); - configXml.getParentFile().mkdirs(); + final File dir = configXml.getParentFile(); + dir.mkdirs(); try { IOUtils.copy(xml,configXml); // load it - TopLevelItem result; - Items.updatingByXml.set(true); - try { - result = (TopLevelItem)Items.load(parent,configXml.getParentFile()); - } finally { - Items.updatingByXml.set(false); - } + TopLevelItem result = Items.whileUpdatingByXml(new Callable() { + @Override public TopLevelItem call() throws IOException { + return (TopLevelItem) Items.load(parent, dir); + } + }); add(result); ItemListener.fireOnCreated(result); @@ -241,25 +266,21 @@ public abstract class ItemGroupMixIn { return result; } catch (IOException e) { // if anything fails, delete the config file to avoid further confusion - Util.deleteRecursive(configXml.getParentFile()); + Util.deleteRecursive(dir); throw e; } } public synchronized TopLevelItem createProject( TopLevelItemDescriptor type, String name, boolean notify ) throws IOException { - acl.checkPermission(Job.CREATE); + acl.checkPermission(Item.CREATE); Jenkins.getInstance().getProjectNamingStrategy().checkName(name); if(parent.getItem(name)!=null) throw new IllegalArgumentException("Project of the name "+name+" already exists"); + // TODO problem with DISCOVER as noted above - TopLevelItem item; - try { - item = type.newInstance(parent,name); - } catch (Exception e) { - throw new IllegalArgumentException(e); - } + TopLevelItem item = type.newInstance(parent, name); try { callOnCreatedFromScratch(item); } catch (AbstractMethodError e) { diff --git a/core/src/main/java/hudson/model/Items.java b/core/src/main/java/hudson/model/Items.java index 746a5aa4fbba066714aa45289c943be9cce23c41..ca8b54fb77333bffb2dc6d69e5ddc914d4137bb3 100644 --- a/core/src/main/java/hudson/model/Items.java +++ b/core/src/main/java/hudson/model/Items.java @@ -26,19 +26,28 @@ package hudson.model; import com.thoughtworks.xstream.XStream; import hudson.DescriptorExtensionList; import hudson.Extension; -import hudson.matrix.MatrixProject; -import hudson.matrix.MatrixConfiguration; import hudson.XmlFile; -import hudson.matrix.Axis; +import hudson.model.listeners.ItemListener; +import hudson.remoting.Callable; import hudson.triggers.Trigger; import hudson.util.DescriptorList; +import hudson.util.EditDistance; import hudson.util.XStream2; import jenkins.model.Jenkins; import org.apache.commons.lang.StringUtils; import java.io.File; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Stack; +import java.util.StringTokenizer; +import javax.annotation.CheckForNull; +import jenkins.model.DirectlyModifiableTopLevelItemGroup; +import org.apache.commons.io.FileUtils; /** * Convenience methods related to {@link Item}. @@ -59,12 +68,41 @@ public class Items { * @see Trigger#start * @since 1.482 */ - static final ThreadLocal updatingByXml = new ThreadLocal() { + private static final ThreadLocal updatingByXml = new ThreadLocal() { @Override protected Boolean initialValue() { return false; } }; + /** + * Runs a block while making {@link #currentlyUpdatingByXml} be temporarily true. + * Use this when you are creating or changing an item. + * @param a return value type (may be {@link Void}) + * @param an error type (may be {@link Error}) + * @param callable a block, typically running {@link #load} or {@link Item#onLoad} + * @return whatever {@code callable} returned + * @throws T anything {@code callable} throws + * @since 1.546 + */ + public static V whileUpdatingByXml(Callable callable) throws T { + updatingByXml.set(true); + try { + return callable.call(); + } finally { + updatingByXml.set(false); + } + } + + /** + * Checks whether we are in the middle of creating or configuring an item via XML. + * Used to determine the {@code newInstance} parameter for {@link Trigger#start}. + * @return true if {@link #whileUpdatingByXml} is currently being called, false for example when merely starting Jenkins or reloading from disk + * @since 1.546 + */ + public static boolean currentlyUpdatingByXml() { + return updatingByXml.get(); + } + /** * Returns all the registered {@link TopLevelItemDescriptor}s. */ @@ -147,8 +185,8 @@ public class Items { } /** - * Compute the relative name of list of items after a rename occurred. Used to manage job references as names in - * plugins to support {@link hudson.model.listeners.ItemListener#onRenamed(hudson.model.Item, String, String)}. + * Computes the relative name of list of items after a rename or move occurred. + * Used to manage job references as names in plugins to support {@link hudson.model.listeners.ItemListener#onLocationChanged}. *

* In a hierarchical context, when a plugin has a reference to a job as ../foo/bar this method will * handle the relative path as "foo" is renamed to "zot" to compute ../zot/bar @@ -168,8 +206,11 @@ public class Items { String canonicalName = getCanonicalName(context, relativeName); if (canonicalName.equals(oldFullName) || canonicalName.startsWith(oldFullName+'/')) { String newCanonicalName = newFullName + canonicalName.substring(oldFullName.length()); - // relative name points to the renamed item, let's compute the new relative name - newValue.add( computeRelativeNameAfterRenaming(canonicalName, newCanonicalName, relativeName) ); + if (relativeName.startsWith("/")) { + newValue.add("/" + newCanonicalName); + } else { + newValue.add(getRelativeNameFrom(newCanonicalName, context.getFullName())); + } } else { newValue.add(relativeName); } @@ -177,39 +218,56 @@ public class Items { return StringUtils.join(newValue, ","); } - /** - * Compute the relative name of an Item after renaming - */ - private static String computeRelativeNameAfterRenaming(String oldFullName, String newFullName, String relativeName) { - - String[] a = oldFullName.split("/"); - String[] n = newFullName.split("/"); - assert a.length == n.length; - String[] r = relativeName.split("/"); - - int j = a.length-1; - for(int i=r.length-1;i>=0;i--) { - String part = r[i]; - if (part.equals("") && i==0) { - continue; - } - if (part.equals(".")) { - continue; - } - if (part.equals("..")) { - j--; - continue; - } - if (part.equals(a[j])) { - r[i] = n[j]; - j--; + // Had difficulty adapting the version in Functions to use no live items, so rewrote it: + static String getRelativeNameFrom(String itemFullName, String groupFullName) { + String[] itemFullNameA = itemFullName.isEmpty() ? new String[0] : itemFullName.split("/"); + String[] groupFullNameA = groupFullName.isEmpty() ? new String[0] : groupFullName.split("/"); + for (int i = 0; ; i++) { + if (i == itemFullNameA.length) { + if (i == groupFullNameA.length) { + // itemFullName and groupFullName are identical + return "."; + } else { + // itemFullName is an ancestor of groupFullName; insert ../ for rest of groupFullName + StringBuilder b = new StringBuilder(); + for (int j = 0; j < groupFullNameA.length - itemFullNameA.length; j++) { + if (j > 0) { + b.append('/'); + } + b.append(".."); + } + return b.toString(); + } + } else if (i == groupFullNameA.length) { + // groupFullName is an ancestor of itemFullName; insert rest of itemFullName + StringBuilder b = new StringBuilder(); + for (int j = i; j < itemFullNameA.length; j++) { + if (j > i) { + b.append('/'); + } + b.append(itemFullNameA[j]); + } + return b.toString(); + } else if (itemFullNameA[i].equals(groupFullNameA[i])) { + // identical up to this point continue; + } else { + // first mismatch; insert ../ for rest of groupFullName, then rest of itemFullName + StringBuilder b = new StringBuilder(); + for (int j = i; j < groupFullNameA.length; j++) { + if (j > i) { + b.append('/'); + } + b.append(".."); + } + for (int j = i; j < itemFullNameA.length; j++) { + b.append('/').append(itemFullNameA[j]); + } + return b.toString(); } } - return StringUtils.join(r, '/'); } - /** * Loads a {@link Item} from a config file. * @@ -244,39 +302,88 @@ public class Items { */ public static List getAllItems(final ItemGroup root, Class type) { List r = new ArrayList(); - - Stack q = new Stack(); - q.push(root); - - while(!q.isEmpty()) { - ItemGroup parent = q.pop(); - for (Item i : parent.getItems()) { - if(type.isInstance(i)) { - if (i.hasPermission(Item.READ)) - r.add(type.cast(i)); - } - if(i instanceof ItemGroup) - q.push((ItemGroup)i); + getAllItems(root, type, r); + return r; + } + private static void getAllItems(final ItemGroup root, Class type, List r) { + List items = new ArrayList(((ItemGroup) root).getItems()); + Collections.sort(items, new Comparator() { + @Override public int compare(Item i1, Item i2) { + return name(i1).compareToIgnoreCase(name(i2)); } - } - // sort by relative name, ignoring case - Collections.sort(r, new Comparator() { - @Override - public int compare(T o1, T o2) { - if (o1 == null) { - if (o2 == null) { - return 0; - } - return 1; + String name(Item i) { + String n = i.getName(); + if (i instanceof ItemGroup) { + n += '/'; } - if (o2 == null) { - return -1; - } - return o1.getRelativeNameFrom(root).compareToIgnoreCase(o2.getRelativeNameFrom(root)); + return n; } - }); - return r; + for (Item i : items) { + if (type.isInstance(i)) { + if (i.hasPermission(Item.READ)) { + r.add(type.cast(i)); + } + } + if (i instanceof ItemGroup) { + getAllItems((ItemGroup) i, type, r); + } + } + } + + /** + * Finds an item whose name (when referenced from the specified context) is closest to the given name. + * @param the type of item being considered + * @param type same as {@code T} + * @param name the supplied name + * @param context a context to start from (used to compute relative names) + * @return the closest available item + * @since 1.538 + */ + public static @CheckForNull T findNearest(Class type, String name, ItemGroup context) { + List projects = Jenkins.getInstance().getAllItems(type); + String[] names = new String[projects.size()]; + for (int i = 0; i < projects.size(); i++) { + names[i] = projects.get(i).getRelativeNameFrom(context); + } + String nearest = EditDistance.findNearest(name, names); + return Jenkins.getInstance().getItem(nearest, context, type); + } + + /** + * Moves an item between folders (or top level). + * Fires all relevant events but does not verify that the item’s directory is not currently being used in some way (for example by a running build). + * Does not check any permissions. + * @param item some item (job or folder) + * @param destination the destination of the move (a folder or {@link Jenkins}); not the current parent (or you could just call {@link AbstractItem#renameTo}) + * @return the new item (usually the same object as {@code item}) + * @throws IOException if the move fails, or some subsequent step fails (directory might have already been moved) + * @throws IllegalArgumentException if the move would really be a rename, or the destination cannot accept the item, or the destination already has an item of that name + * @since 1.548 + */ + public static I move(I item, DirectlyModifiableTopLevelItemGroup destination) throws IOException, IllegalArgumentException { + DirectlyModifiableTopLevelItemGroup oldParent = (DirectlyModifiableTopLevelItemGroup) item.getParent(); + if (oldParent == destination) { + throw new IllegalArgumentException(); + } + // TODO verify that destination is to not equal to, or inside, item + if (!destination.canAdd(item)) { + throw new IllegalArgumentException(); + } + String name = item.getName(); + if (destination.getItem(name) != null) { + throw new IllegalArgumentException(name + " already exists"); + } + String oldFullName = item.getFullName(); + // TODO AbstractItem.renameTo has a more baroque implementation; factor it out into a utility method perhaps? + File destDir = destination.getRootDirFor(item); + FileUtils.forceMkdir(destDir.getParentFile()); + FileUtils.moveDirectory(item.getRootDir(), destDir); + oldParent.remove(item); + I newItem = destination.add(item, name); + newItem.onLoad(destination, name); + ItemListener.fireLocationChange(newItem, oldFullName); + return newItem; } /** @@ -294,8 +401,5 @@ public class Items { static { XSTREAM.alias("project",FreeStyleProject.class); - XSTREAM.alias("matrix-project",MatrixProject.class); - XSTREAM.alias("axis", Axis.class); - XSTREAM.alias("matrix-config",MatrixConfiguration.class); } } diff --git a/core/src/main/java/hudson/model/JDK.java b/core/src/main/java/hudson/model/JDK.java index 5841620aee6360ae42a2d312ba01a4369921611e..27a7810ce085e686421c430bd2013f2941e31b2c 100644 --- a/core/src/main/java/hudson/model/JDK.java +++ b/core/src/main/java/hudson/model/JDK.java @@ -23,8 +23,6 @@ */ package hudson.model; -import com.infradna.tool.bridge_method_injector.BridgeMethodsAdded; -import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import hudson.util.StreamTaskListener; import hudson.util.NullStream; import hudson.util.FormValidation; @@ -47,7 +45,6 @@ import java.util.Collections; import jenkins.model.Jenkins; import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.QueryParameter; /** * Information about JDK installation. @@ -175,16 +172,7 @@ public final class JDK extends ToolInstallation implements NodeSpecific, En /** * Checks if the JAVA_HOME is a valid JAVA_HOME path. */ - public FormValidation doCheckHome(@QueryParameter File value) { - // this can be used to check the existence of a file on the server, so needs to be protected - Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); - - if(value.getPath().equals("")) - return FormValidation.ok(); - - if(!value.isDirectory()) - return FormValidation.error(Messages.Hudson_NotADirectory(value)); - + @Override protected FormValidation checkHomeDirectory(File value) { File toolsJar = new File(value,"lib/tools.jar"); File mac = new File(value,"lib/dt.jar"); if(!toolsJar.exists() && !mac.exists()) @@ -193,9 +181,6 @@ public final class JDK extends ToolInstallation implements NodeSpecific, En return FormValidation.ok(); } - public FormValidation doCheckName(@QueryParameter String value) { - return FormValidation.validateRequired(value); - } } public static class ConverterImpl extends ToolConverter { diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java index aef474184ac66d21114e69e82c51c22bac4a1069..3f7cb46469a42a511a713a8e326c2472e17b6932 100644 --- a/core/src/main/java/hudson/model/Job.java +++ b/core/src/main/java/hudson/model/Job.java @@ -54,6 +54,7 @@ import hudson.util.DescribableList; import hudson.util.FormApply; import hudson.util.Graph; import hudson.util.ProcessTree; +import hudson.util.QuotedStringTokenizer; import hudson.util.RunList; import hudson.util.ShiftedCategoryAxis; import hudson.util.StackedAreaRenderer2; @@ -95,8 +96,11 @@ import java.io.*; import java.net.URLEncoder; import java.util.*; import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import static javax.servlet.http.HttpServletResponse.*; +import jenkins.model.lazy.LazyBuildMixIn; /** * A job is an runnable entity under the monitoring of Hudson. @@ -354,13 +358,15 @@ public abstract class Job, RunT extends Run, RunT extends Run(this, getBuilds(), HISTORY_ADAPTER); } - protected static final HistoryWidget.Adapter HISTORY_ADAPTER = new Adapter() { + public static final HistoryWidget.Adapter HISTORY_ADAPTER = new Adapter() { public int compare(Run record, String key) { try { int k = Integer.parseInt(key); @@ -614,7 +623,7 @@ public abstract class Job, RunT extends Run, RunT extends Run, RunT extends Run, RunT extends Run m = _getRuns().headMap(n - 1); // the map should @@ -735,6 +745,7 @@ public abstract class Job, RunT extends Run m = _getRuns().tailMap(n); @@ -776,7 +787,11 @@ public abstract class Job, RunT extends Run, RunT extends Run _getRuns(); @@ -792,11 +808,13 @@ public abstract class Job, RunT extends Run, RunT extends Run, RunT extends Run getEstimatedDurationCandidates() { List candidates = new ArrayList(3); - RunT lastSuccessful = (RunT) Permalink.LAST_SUCCESSFUL_BUILD.resolve(this); + RunT lastSuccessful = getLastSuccessfulBuild(); int lastSuccessfulNumber = -1; if (lastSuccessful != null) { candidates.add(lastSuccessful); @@ -925,7 +944,7 @@ public abstract class Job, RunT extends Run fallbackCandidates = new ArrayList(3); while (r != null && candidates.size() < 3 && i < 6) { if (!r.isBuilding() && r.getResult() != null && r.getNumber() != lastSuccessfulNumber) { @@ -972,11 +991,8 @@ public abstract class Job, RunT extends Run, RunT extends Run, JobPropertyDescriptor> t = new DescribableList, JobPropertyDescriptor>(NOOP,getAllProperties()); - t.rebuild(req,json.optJSONObject("properties"),JobPropertyDescriptor.getPropertyDescriptors(Job.this.getClass())); + JSONObject jsonProperties = json.optJSONObject("properties"); + if (jsonProperties != null) { + t.rebuild(req,jsonProperties,JobPropertyDescriptor.getPropertyDescriptors(Job.this.getClass())); + } else { + t.clear(); + } properties.clear(); for (JobProperty p : t) { p.setOwner(this); @@ -1144,7 +1165,11 @@ public abstract class Job, RunT extends Run, RunT extends Run { @@ -1269,7 +1294,7 @@ public abstract class Job, RunT extends Run data = new DataSetBuilder(); - for (Run r : getBuilds()) { + for (Run r : getNewBuilds()) { if (r.isBuilding()) continue; data.add(((double) r.getDuration()) / (1000 * 60), "min", @@ -1345,6 +1370,16 @@ public abstract class Job, RunT extends Run build, EnvVars env) { + public void buildEnvironment(Run build, EnvVars env) { // TODO: check with Tom if this is really what he had in mind env.put(name,job.toString()); env.put(name.toUpperCase(Locale.ENGLISH),job.toString()); // backward compatibility pre 1.345 diff --git a/core/src/main/java/hudson/model/ListView.java b/core/src/main/java/hudson/model/ListView.java index 7031bdad958a6fd88d1cb43cb9fcac7bc7542b3f..19f8739494ed99fd123b8e9ca60ef56b71848e4a 100644 --- a/core/src/main/java/hudson/model/ListView.java +++ b/core/src/main/java/hudson/model/ListView.java @@ -28,6 +28,7 @@ import hudson.Extension; import hudson.Util; import hudson.diagnosis.OldDataMonitor; import hudson.model.Descriptor.FormException; +import hudson.model.listeners.ItemListener; import hudson.util.CaseInsensitiveComparator; import hudson.util.DescribableList; import hudson.util.FormValidation; @@ -37,13 +38,18 @@ import hudson.views.ViewJobFilter; import java.io.IOException; import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import javax.annotation.concurrent.GuardedBy; import javax.servlet.ServletException; +import jenkins.model.Jenkins; import net.sf.json.JSONObject; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.HttpResponse; @@ -57,7 +63,7 @@ import org.kohsuke.stapler.interceptor.RequirePOST; * * @author Kohsuke Kawaguchi */ -public class ListView extends View implements Saveable { +public class ListView extends View implements DirectlyModifiableView { /** * List of job names. This is what gets serialized. @@ -140,6 +146,7 @@ public class ListView extends View implements Saveable { return jobFilters; } + @Override public DescribableList> getColumns() { return columns; } @@ -151,6 +158,7 @@ public class ListView extends View implements Saveable { * This method returns a separate copy each time to avoid * concurrent modification issue. */ + @Override public List getItems() { SortedSet names; List items = new ArrayList(); @@ -164,7 +172,13 @@ public class ListView extends View implements Saveable { includeItems(parent, parentItems, names); Boolean statusFilter = this.statusFilter; // capture the value to isolate us from concurrent update - for (TopLevelItem item : Items.getAllItems(getOwnerItemGroup(), TopLevelItem.class)) { + Iterable candidates; + if (recurse) { + candidates = Items.getAllItems(parent, TopLevelItem.class); + } else { + candidates = parent.getItems(); + } + for (TopLevelItem item : candidates) { if (!names.contains(item.getRelativeNameFrom(getOwnerItemGroup()))) continue; // Add if no status filter or filter matches enabled/disabled status: if(statusFilter == null || !(item instanceof AbstractProject) @@ -175,6 +189,7 @@ public class ListView extends View implements Saveable { // check the filters Iterable jobFilters = getJobFilters(); List allItems = new ArrayList(parentItems); + if (recurse) allItems = expand(allItems, new ArrayList()); for (ViewJobFilter jobFilter: jobFilters) { items = jobFilter.filter(items, allItems, this); } @@ -183,6 +198,17 @@ public class ListView extends View implements Saveable { return items; } + + private List expand(Collection items, List allItems) { + for (TopLevelItem item : items) { + if (item instanceof ItemGroup) { + ItemGroup ig = (ItemGroup) item; + expand(Util.filter(ig.getItems(), TopLevelItem.class), allItems); + } + allItems.add(item); + } + return allItems; + } @Override public boolean contains(TopLevelItem item) { @@ -211,13 +237,12 @@ public class ListView extends View implements Saveable { return jobNames.contains(item.getRelativeNameFrom(getOwnerItemGroup())); } - - /** * Adds the given item to this view. * * @since 1.389 */ + @Override public void add(TopLevelItem item) throws IOException { synchronized (this) { jobNames.add(item.getRelativeNameFrom(getOwnerItemGroup())); @@ -225,6 +250,21 @@ public class ListView extends View implements Saveable { save(); } + /** + * Removes given item from this view. + * + * @since 1.566 + */ + @Override + public boolean remove(TopLevelItem item) throws IOException { + synchronized (this) { + String name = item.getRelativeNameFrom(getOwnerItemGroup()); + if (!jobNames.remove(name)) return false; + } + save(); + return true; + } + public String getIncludeRegex() { return includeRegex; } @@ -233,10 +273,10 @@ public class ListView extends View implements Saveable { return recurse; } - /* - * For testing purposes + /** + * @since 1.568 */ - void setRecurse(boolean recurse) { + public void setRecurse(boolean recurse) { this.recurse = recurse; } @@ -248,6 +288,8 @@ public class ListView extends View implements Saveable { return statusFilter; } + @Override + @RequirePOST public Item doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { ItemGroup ig = getOwnerItemGroup(); if (ig instanceof ModifiableItemGroup) { @@ -263,37 +305,46 @@ public class ListView extends View implements Saveable { return null; } + @Override @RequirePOST public HttpResponse doAddJobToView(@QueryParameter String name) throws IOException, ServletException { checkPermission(View.CONFIGURE); if(name==null) throw new Failure("Query parameter 'name' is required"); - if (getOwnerItemGroup().getItem(name) == null) + TopLevelItem item = resolveName(name); + if (item == null) throw new Failure("Query parameter 'name' does not correspond to a known item"); - if (jobNames.add(name)) - owner.save(); + if (contains(item)) return HttpResponses.ok(); + + add(item); + owner.save(); return HttpResponses.ok(); } + @Override @RequirePOST public HttpResponse doRemoveJobFromView(@QueryParameter String name) throws IOException, ServletException { checkPermission(View.CONFIGURE); if(name==null) throw new Failure("Query parameter 'name' is required"); - if (jobNames.remove(name)) + TopLevelItem item = resolveName(name); + if (remove(item)) owner.save(); return HttpResponses.ok(); } - @Override - public synchronized void onJobRenamed(Item item, String oldName, String newName) { - if(jobNames.remove(oldName) && newName!=null) - jobNames.add(newName); + private TopLevelItem resolveName(String name) { + TopLevelItem item = getOwnerItemGroup().getItem(name); + if (item == null) { + name = Items.getCanonicalName(getOwnerItemGroup(), name); + item = Jenkins.getInstance().getItemByFullName(name, TopLevelItem.class); + } + return item; } /** @@ -348,6 +399,7 @@ public class ListView extends View implements Saveable { @Extension public static class DescriptorImpl extends ViewDescriptor { + @Override public String getDisplayName() { return Messages.ListView_DisplayName(); } @@ -372,7 +424,64 @@ public class ListView extends View implements Saveable { * @deprecated as of 1.391 * Use {@link ListViewColumn#createDefaultInitialColumnList()} */ + @Deprecated public static List getDefaultColumns() { return ListViewColumn.createDefaultInitialColumnList(); } + + @Restricted(NoExternalUse.class) + @Extension public static final class Listener extends ItemListener { + @Override public void onLocationChanged(Item item, String oldFullName, String newFullName) { + for (Item g : Jenkins.getInstance().getAllItems()) { + if (g instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) g; + for (View v : vg.getViews()) { + if (v instanceof ListView) { + ListView lv = (ListView) v; + boolean needsSave; + synchronized (lv) { + Set oldJobNames = new HashSet(lv.jobNames); + lv.jobNames.clear(); + for (String oldName : oldJobNames) { + lv.jobNames.add(Items.computeRelativeNamesAfterRenaming(oldFullName, newFullName, oldName, vg.getItemGroup())); + } + needsSave = !oldJobNames.equals(lv.jobNames); + } + if (needsSave) { // do not hold ListView lock at the time + try { + g.save(); + } catch (IOException x) { + Logger.getLogger(ListView.class.getName()).log(Level.WARNING, null, x); + } + } + } + } + } + } + } + @Override public void onDeleted(Item item) { + for (Item g : Jenkins.getInstance().getAllItems()) { + if (g instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) g; + for (View v : vg.getViews()) { + if (v instanceof ListView) { + ListView lv = (ListView) v; + boolean needsSave; + synchronized (lv) { + needsSave = lv.jobNames.remove(item.getRelativeNameFrom(vg.getItemGroup())); + } + if (needsSave) { + try { + g.save(); + } catch (IOException x) { + Logger.getLogger(ListView.class.getName()).log(Level.WARNING, null, x); + } + } + } + } + } + } + } + } + } diff --git a/core/src/main/java/hudson/model/LoadStatistics.java b/core/src/main/java/hudson/model/LoadStatistics.java index 940768564f2a16483a31699cfe9777c7d0b7f242..b996f040b465294f97a98e1f14d3ead8b70f625a 100644 --- a/core/src/main/java/hudson/model/LoadStatistics.java +++ b/core/src/main/java/hudson/model/LoadStatistics.java @@ -26,6 +26,8 @@ package hudson.model; import hudson.Extension; import hudson.model.MultiStageTimeSeries.TimeScale; import hudson.model.MultiStageTimeSeries.TrendChart; +import hudson.model.queue.SubTask; +import hudson.model.queue.Tasks; import hudson.util.ColorPalette; import hudson.util.NoOverlapCategoryAxis; import jenkins.model.Jenkins; @@ -230,8 +232,9 @@ public abstract class LoadStatistics { private int count(List bis, Label l) { int q=0; for (Queue.BuildableItem bi : bis) { - if(bi.getAssignedLabel()==l) - q++; + for (SubTask st : Tasks.getSubTasksOf(bi.task)) + if (bi.getAssignedLabelFor(st)==l) + q++; } return q; } diff --git a/core/src/main/java/hudson/model/ManageJenkinsAction.java b/core/src/main/java/hudson/model/ManageJenkinsAction.java index 198f6a3e3ba4d919ff649963223b3437b4d37d5a..a4b813a85256676d8c1182a9ea2e9fc495941cc0 100644 --- a/core/src/main/java/hudson/model/ManageJenkinsAction.java +++ b/core/src/main/java/hudson/model/ManageJenkinsAction.java @@ -24,6 +24,7 @@ package hudson.model; import hudson.Extension; +import jenkins.model.Jenkins; /** * Adds the "Manage Jenkins" link to the top page. @@ -33,7 +34,7 @@ import hudson.Extension; @Extension(ordinal=100) public class ManageJenkinsAction implements RootAction { public String getIconFileName() { - if (Hudson.getInstance().hasPermission(Hudson.ADMINISTER)) + if (Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) return "setting.png"; else return null; diff --git a/core/src/main/java/hudson/model/ModelObject.java b/core/src/main/java/hudson/model/ModelObject.java index d0087d37b1464b9584e3a5a772a8f421ae0dc3a6..f6368a13fad372e6811c13841a7a97e0f61ed6c7 100644 --- a/core/src/main/java/hudson/model/ModelObject.java +++ b/core/src/main/java/hudson/model/ModelObject.java @@ -24,8 +24,12 @@ package hudson.model; /** - * A model object has a URL. - * + * A model object has a human readable name. + * + * And it normally has URL, but this interface doesn't define one. + * (Since there's so many classes that define the getUrl method + * we should have such one.) + * * @author Kohsuke Kawaguchi */ public interface ModelObject { diff --git a/core/src/main/java/hudson/model/ModifiableViewGroup.java b/core/src/main/java/hudson/model/ModifiableViewGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..6a2542af387e87c2fd2eb78f5c63b0b267fe186a --- /dev/null +++ b/core/src/main/java/hudson/model/ModifiableViewGroup.java @@ -0,0 +1,42 @@ +/* + * The MIT License + * + * Copyright (c) 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package hudson.model; + +import java.io.IOException; + +import javax.annotation.Nonnull; + +/** + * {@link ViewGroup} that can be modified. + * + * @author ogondza + * @since 1.545 + */ +public interface ModifiableViewGroup extends ViewGroup { + + /** + * Add new {@link View} to this {@link ViewGroup}. + */ + public void addView(@Nonnull View view) throws IOException; +} diff --git a/core/src/main/java/hudson/model/MultiStageTimeSeries.java b/core/src/main/java/hudson/model/MultiStageTimeSeries.java index b9de7e785a7d38c346e6f76244aa307b87dc2bba..33dbee6b8226fa1debfeac778f92ca6cba974e60 100644 --- a/core/src/main/java/hudson/model/MultiStageTimeSeries.java +++ b/core/src/main/java/hudson/model/MultiStageTimeSeries.java @@ -95,6 +95,9 @@ public class MultiStageTimeSeries implements Serializable { private int counter; + private static final Font CHART_FONT = Font.getFont(MultiStageTimeSeries.class.getName() + ".chartFont", + new Font(Font.SANS_SERIF, Font.PLAIN, 10)); + public MultiStageTimeSeries(Localizable title, Color color, float initialValue, float decay) { this.title = title; this.color = color; @@ -242,6 +245,7 @@ public class MultiStageTimeSeries implements Serializable { ); chart.setBackgroundPaint(Color.white); + chart.getLegend().setItemFont(CHART_FONT); final CategoryPlot plot = chart.getCategoryPlot(); configurePlot(plot); @@ -255,6 +259,8 @@ public class MultiStageTimeSeries implements Serializable { protected void configureRangeAxis(NumberAxis rangeAxis) { rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); + rangeAxis.setTickLabelFont(CHART_FONT); + rangeAxis.setLabelFont(CHART_FONT); } protected void crop(CategoryPlot plot) { @@ -269,6 +275,8 @@ public class MultiStageTimeSeries implements Serializable { domainAxis.setLowerMargin(0.0); domainAxis.setUpperMargin(0.0); domainAxis.setCategoryMargin(0.0); + domainAxis.setLabelFont(CHART_FONT); + domainAxis.setTickLabelFont(CHART_FONT); return domainAxis; } diff --git a/core/src/main/java/hudson/model/MyView.java b/core/src/main/java/hudson/model/MyView.java index 0d61be1d7cd81a577e4a6d5243f686b854b10414..74bdadf9da5ff58ae804fefa425bdbb5950a3e05 100644 --- a/core/src/main/java/hudson/model/MyView.java +++ b/core/src/main/java/hudson/model/MyView.java @@ -37,6 +37,7 @@ import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.DataBoundConstructor; import hudson.model.Descriptor.FormException; import hudson.Extension; +import org.kohsuke.stapler.interceptor.RequirePOST; /** * {@link View} that only contains projects for which the current user has access to. @@ -57,9 +58,10 @@ public class MyView extends View { @Override public boolean contains(TopLevelItem item) { - return item.hasPermission(Job.CONFIGURE); + return item.hasPermission(Item.CONFIGURE); } + @RequirePOST @Override public TopLevelItem doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { @@ -74,7 +76,7 @@ public class MyView extends View { public Collection getItems() { List items = new ArrayList(); for (TopLevelItem item : getOwnerItemGroup().getItems()) { - if (item.hasPermission(Job.CONFIGURE)) { + if (item.hasPermission(Item.CONFIGURE)) { items.add(item); } } @@ -86,11 +88,6 @@ public class MyView extends View { return ""; // there's no configuration page } - @Override - public void onJobRenamed(Item item, String oldName, String newName) { - // noop - } - @Override protected void submit(StaplerRequest req) throws IOException, ServletException, FormException { // noop diff --git a/core/src/main/java/hudson/model/MyViewsProperty.java b/core/src/main/java/hudson/model/MyViewsProperty.java index 3f65ba8d3b4b03a822d728986b94ef718c781845..a7a40014e8227930eb13a9462978a1d45691936e 100644 --- a/core/src/main/java/hudson/model/MyViewsProperty.java +++ b/core/src/main/java/hudson/model/MyViewsProperty.java @@ -58,7 +58,7 @@ import org.kohsuke.stapler.StaplerResponse; * * @author Tom Huybrechts */ -public class MyViewsProperty extends UserProperty implements ViewGroup, Action, StaplerFallback { +public class MyViewsProperty extends UserProperty implements ModifiableViewGroup, Action, StaplerFallback { private String primaryViewName; /** @@ -136,6 +136,7 @@ public class MyViewsProperty extends UserProperty implements ViewGroup, Action, viewGroupMixIn.onViewRenamed(view,oldName,newName); } + @Override public void addView(View view) throws IOException { viewGroupMixIn.addView(view); } diff --git a/core/src/main/java/hudson/model/Node.java b/core/src/main/java/hudson/model/Node.java index f542b784112c43bfd8dcd6c34438c0b4eb8f8d40..03f2c2413e2065aab46da22c33f87f6a07d8b11f 100644 --- a/core/src/main/java/hudson/model/Node.java +++ b/core/src/main/java/hudson/model/Node.java @@ -25,11 +25,7 @@ package hudson.model; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; -import hudson.Extension; -import hudson.ExtensionPoint; -import hudson.FilePath; -import hudson.FileSystemProvisioner; -import hudson.Launcher; +import hudson.*; import hudson.model.Descriptor.FormException; import hudson.model.Queue.Task; import hudson.model.labels.LabelAtom; @@ -103,7 +99,11 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable } public String getSearchUrl() { - return "computer/"+getNodeName(); + Computer c = toComputer(); + if (c != null) { + return c.getUrl(); + } + return "computer/" + Util.rawEncode(getNodeName()); } public boolean isHoldOffLaunchUntilSave() { @@ -117,7 +117,7 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable * "" if this is master */ @Exported(visibility=999) - public abstract String getNodeName(); + public abstract @Nonnull String getNodeName(); /** * When the user clones a {@link Node}, Hudson uses this method to change the node name right after @@ -202,7 +202,7 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable // At startup, we need to restore any previously in-effect temp offline cause. // We wait until the computer is started rather than getting the data to it sooner // so that the normal computer start up processing works as expected. - if (node.temporaryOfflineCause != null && node.temporaryOfflineCause != c.getOfflineCause()) { + if (node!= null && node.temporaryOfflineCause != null && node.temporaryOfflineCause != c.getOfflineCause()) { c.setTemporarilyOffline(true, node.temporaryOfflineCause); } } @@ -345,7 +345,6 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable Authentication identity = item.authenticate(); if (!getACL().hasPermission(identity,Computer.BUILD)) { // doesn't have a permission - // TODO: does it make more sense to define a separate permission? return CauseOfBlockage.fromMessage(Messages._Node_LackingBuildPermission(identity.getName(),getNodeName())); } @@ -371,7 +370,7 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable * null if this node is not connected hence the path is not available */ // TODO: should this be modified now that getWorkspace is moved from AbstractProject to AbstractBuild? - public abstract FilePath getWorkspaceFor(TopLevelItem item); + public abstract @CheckForNull FilePath getWorkspaceFor(TopLevelItem item); /** * Gets the root directory of this node. @@ -384,7 +383,7 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable * null if the node is offline and hence the {@link FilePath} * object is not available. */ - public abstract FilePath getRootPath(); + public abstract @CheckForNull FilePath getRootPath(); /** * Gets the {@link FilePath} on this node. diff --git a/core/src/main/java/hudson/model/OneOffExecutor.java b/core/src/main/java/hudson/model/OneOffExecutor.java index 6c55263093efd750360ed7895a66140c3635b38a..9c869c579c70dcc7d4f0b247066642fa92b1b828 100644 --- a/core/src/main/java/hudson/model/OneOffExecutor.java +++ b/core/src/main/java/hudson/model/OneOffExecutor.java @@ -24,7 +24,6 @@ package hudson.model; import hudson.model.Queue.FlyweightTask; -import hudson.model.queue.WorkUnit; /** * {@link Executor} that's temporarily added to carry out tasks that doesn't consume @@ -34,30 +33,8 @@ import hudson.model.queue.WorkUnit; * @see FlyweightTask */ public class OneOffExecutor extends Executor { - private WorkUnit work; - - public OneOffExecutor(Computer owner, WorkUnit work) { + public OneOffExecutor(Computer owner) { super(owner,-1); - this.work = work; - } - - @Override - protected boolean shouldRun() { - // TODO: consulting super.shouldRun() here means we'll lose the work if it gets scheduled - // when super.shouldRun() returns false. - return super.shouldRun() && work !=null; - } - - public WorkUnit getAssignedWorkUnit() { - return work; - } - - @Override - protected WorkUnit grabJob() throws InterruptedException { - WorkUnit r = super.grabJob(); - assert r==work; - work = null; - return r; } @Override diff --git a/core/src/main/java/hudson/model/PaneStatusProperties.java b/core/src/main/java/hudson/model/PaneStatusProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..9f8be772279f2623ae959beec3488e606bc77959 --- /dev/null +++ b/core/src/main/java/hudson/model/PaneStatusProperties.java @@ -0,0 +1,98 @@ +package hudson.model; + +import static java.lang.String.format; +import hudson.Extension; +import hudson.util.PersistedList; + +import java.io.IOException; + +import javax.servlet.http.HttpSession; + +import org.kohsuke.stapler.Stapler; + +public class PaneStatusProperties extends UserProperty implements Saveable { + + private final PersistedList collapsed = new PersistedList(this); + + private static final PaneStatusProperties FALLBACK = new PaneStatusPropertiesSessionFallback(); + + public boolean isCollapsed(String paneId) { + return collapsed.contains(paneId); + } + + /** + * @param paneId panel name + * @return the actual state of panel + */ + public boolean toggleCollapsed(String paneId) throws IOException { + if (collapsed.contains(paneId)) { + collapsed.remove(paneId); + return false; + } else { + collapsed.add(paneId); + return true; + } + } + + public void save() throws IOException { + user.save(); + } + + private Object readResolve() { + collapsed.setOwner(this); + return this; + } + + @Extension + public static class DescriptorImpl extends UserPropertyDescriptor { + + @Override + public UserProperty newInstance(User user) { + return new PaneStatusProperties(); + } + + @Override + public String getDisplayName() { + return null; + } + + @Override + public boolean isEnabled() { + return false; + } + + } + + private static class PaneStatusPropertiesSessionFallback extends PaneStatusProperties { + + private final String attribute = "jenkins_pane_%s_collapsed"; + + @Override + public boolean isCollapsed(String paneId) { + final HttpSession session = Stapler.getCurrentRequest().getSession(); + return session.getAttribute(format(attribute, paneId)) != null; + } + + @Override + public boolean toggleCollapsed(String paneId) { + final HttpSession session = Stapler.getCurrentRequest().getSession(); + final String property = format(attribute, paneId); + final Object collapsed = session.getAttribute(property); + if (collapsed == null) { + session.setAttribute(property, true); + return true; + } + session.removeAttribute(property); + return false; + } + } + + public static PaneStatusProperties forCurrentUser() { + final User current = User.current(); + if (current == null) { + return FALLBACK; + } + return current.getProperty(PaneStatusProperties.class); + } + +} diff --git a/core/src/main/java/hudson/model/ParameterDefinition.java b/core/src/main/java/hudson/model/ParameterDefinition.java index 32c89fb00a6a40d58849c9d7f1a9473675c9cd3c..fcc1a5cca8a1af45a8fc82a0bd4963170a7725ff 100644 --- a/core/src/main/java/hudson/model/ParameterDefinition.java +++ b/core/src/main/java/hudson/model/ParameterDefinition.java @@ -33,6 +33,7 @@ import hudson.util.DescriptorList; import java.io.Serializable; import java.io.IOException; import java.util.logging.Logger; +import javax.annotation.CheckForNull; import jenkins.model.Jenkins; import net.sf.json.JSONObject; @@ -45,7 +46,7 @@ import org.kohsuke.stapler.export.ExportedBean; * Defines a parameter for a build. * *

- * In Hudson, a user can configure a job to require parameters for a build. + * In Jenkins, a user can configure a job to require parameters for a build. * For example, imagine a test job that takes the bits to be tested as a parameter. * *

@@ -165,6 +166,7 @@ public abstract class ParameterDefinition implements * This method is invoked when the user fills in the parameter values in the HTML form * and submits it to the server. */ + @CheckForNull public abstract ParameterValue createValue(StaplerRequest req, JSONObject jo); /** @@ -183,6 +185,7 @@ public abstract class ParameterDefinition implements * @throws IllegalStateException * If the parameter is deemed required but was missing in the submission. */ + @CheckForNull public abstract ParameterValue createValue(StaplerRequest req); @@ -200,6 +203,7 @@ public abstract class ParameterDefinition implements * the command exits with an error code. * @since 1.334 */ + @CheckForNull public ParameterValue createValue(CLICommand command, String value) throws IOException, InterruptedException { throw new AbortException("CLI parameter submission is not supported for the "+getClass()+" type. Please file a bug report for this"); } @@ -210,6 +214,7 @@ public abstract class ParameterDefinition implements * @return default parameter value or null if no defaults are available * @since 1.253 */ + @CheckForNull @Exported public ParameterValue getDefaultParameterValue() { return null; diff --git a/core/src/main/java/hudson/model/ParameterValue.java b/core/src/main/java/hudson/model/ParameterValue.java index 84898581322a045785a5c91147ff89dc182c37f6..8bafa5aa766f9ceebb802664c96da70bf689540a 100644 --- a/core/src/main/java/hudson/model/ParameterValue.java +++ b/core/src/main/java/hudson/model/ParameterValue.java @@ -125,16 +125,31 @@ public abstract class ParameterValue implements Serializable { * @param build * The build for which this parameter is being used. Never null. * @deprecated as of 1.344 - * Use {@link #buildEnvVars(AbstractBuild, EnvVars)} instead. + * Use {@link #buildEnvironment(Run, EnvVars)} instead. */ public void buildEnvVars(AbstractBuild build, Map env) { - if (env instanceof EnvVars && Util.isOverridden(ParameterValue.class,getClass(),"buildEnvVars", AbstractBuild.class,EnvVars.class)) - // if the subtype already derives buildEnvVars(AbstractBuild,Map), then delegate to it - buildEnvVars(build,(EnvVars)env); - + if (env instanceof EnvVars) { + if (Util.isOverridden(ParameterValue.class, getClass(), "buildEnvironment", Run.class, EnvVars.class)) { + // if the subtype already derives buildEnvironment, then delegate to it + buildEnvironment(build, (EnvVars) env); + } else if (Util.isOverridden(ParameterValue.class, getClass(), "buildEnvVars", AbstractBuild.class, EnvVars.class)) { + buildEnvVars(build, (EnvVars) env); + } + } // otherwise no-op by default } + /** @deprecated Use {@link #buildEnvironment(Run, EnvVars)} instead. */ + @Deprecated + public void buildEnvVars(AbstractBuild build, EnvVars env) { + if (Util.isOverridden(ParameterValue.class, getClass(), "buildEnvironment", Run.class, EnvVars.class)) { + buildEnvironment(build, env); + } else { + // for backward compatibility + buildEnvVars(build,(Map)env); + } + } + /** * Adds environmental variables for the builds to the given map. * @@ -151,10 +166,13 @@ public abstract class ParameterValue implements Serializable { * never null. * @param build * The build for which this parameter is being used. Never null. + * @since 1.556 */ - public void buildEnvVars(AbstractBuild build, EnvVars env) { - // for backward compatibility - buildEnvVars(build,(Map)env); + public void buildEnvironment(Run build, EnvVars env) { + if (build instanceof AbstractBuild) { + buildEnvVars((AbstractBuild) build, env); + } + // else do not know how to do it } /** @@ -193,6 +211,8 @@ public abstract class ParameterValue implements Serializable { return VariableResolver.NONE; } + // TODO should there be a Run overload of this? + /** * Accessing {@link ParameterDefinition} is not a good idea. * @@ -258,6 +278,17 @@ public abstract class ParameterValue implements Serializable { return false; } + /** + * Returns the most natural Java object that represents the actual value, like + * boolean, string, etc. + * + * If there's nothing that really fits the bill, the callee can return {@code this}. + * @since 1.568 + */ + public Object getValue() { + return null; + } + /** * Controls where the build (that this parameter is submitted to) will happen. * diff --git a/core/src/main/java/hudson/model/ParametersAction.java b/core/src/main/java/hudson/model/ParametersAction.java index c5cab54deade1e34aa8ed49eca26a4c7d43eb100..85ed085c80859cc4c24a2b74e8ec944e2771e28d 100644 --- a/core/src/main/java/hudson/model/ParametersAction.java +++ b/core/src/main/java/hudson/model/ParametersAction.java @@ -26,7 +26,6 @@ package hudson.model; import hudson.Util; import hudson.EnvVars; import hudson.diagnosis.OldDataMonitor; -import hudson.matrix.MatrixChildAction; import hudson.model.Queue.QueueAction; import hudson.model.labels.LabelAssignmentAction; import hudson.model.queue.SubTask; @@ -36,7 +35,6 @@ import hudson.util.VariableResolver; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -45,6 +43,11 @@ import java.util.Iterator; import java.util.List; import java.util.Set; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Sets.newHashSet; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + /** * Records the parameter values used for a build. * @@ -54,7 +57,7 @@ import java.util.Set; * that were specified when scheduling. */ @ExportedBean -public class ParametersAction implements Action, Iterable, QueueAction, EnvironmentContributingAction, LabelAssignmentAction, MatrixChildAction { +public class ParametersAction implements Action, Iterable, QueueAction, EnvironmentContributingAction, LabelAssignmentAction { private final List parameters; @@ -73,16 +76,21 @@ public class ParametersAction implements Action, Iterable, Queue public void createBuildWrappers(AbstractBuild build, Collection result) { for (ParameterValue p : parameters) { + if (p == null) continue; BuildWrapper w = p.createBuildWrapper(build); if(w!=null) result.add(w); } } public void buildEnvVars(AbstractBuild build, EnvVars env) { - for (ParameterValue p : parameters) - p.buildEnvVars(build,env); + for (ParameterValue p : parameters) { + if (p == null) continue; + p.buildEnvironment(build, env); + } } + // TODO do we need an EnvironmentContributingAction variant that takes Run so this can implement it? + /** * Performs a variable substitution to the given text and return it. */ @@ -99,14 +107,16 @@ public class ParametersAction implements Action, Iterable, Queue public VariableResolver createVariableResolver(AbstractBuild build) { VariableResolver[] resolvers = new VariableResolver[parameters.size()+1]; int i=0; - for (ParameterValue p : parameters) + for (ParameterValue p : parameters) { + if (p == null) continue; resolvers[i++] = p.createVariableResolver(build); - + } + resolvers[i] = build.getBuildVariableResolver(); return new VariableResolver.Union(resolvers); } - + public Iterator iterator() { return parameters.iterator(); } @@ -117,14 +127,17 @@ public class ParametersAction implements Action, Iterable, Queue } public ParameterValue getParameter(String name) { - for (ParameterValue p : parameters) + for (ParameterValue p : parameters) { + if (p == null) continue; if (p.getName().equals(name)) return p; + } return null; } public Label getAssignedLabel(SubTask task) { for (ParameterValue p : parameters) { + if (p == null) continue; Label l = p.getAssignedLabel(task); if (l!=null) return l; } @@ -163,24 +176,42 @@ public class ParametersAction implements Action, Iterable, Queue /** * Creates a new {@link ParametersAction} that contains all the parameters in this action * with the overrides / new values given as parameters. + * @return New {@link ParametersAction}. The result may contain null {@link ParameterValue}s */ - public ParametersAction createUpdated(Collection newValues) { - List r = new ArrayList(); + @Nonnull + public ParametersAction createUpdated(Collection overrides) { + if(overrides == null) { + return new ParametersAction(parameters); + } + List combinedParameters = newArrayList(overrides); + Set names = newHashSet(); - Set names = new HashSet(); - for (ParameterValue v : newValues) { - names.add(v.name); + for(ParameterValue v : overrides) { + if (v == null) continue; + names.add(v.getName()); } - for (Iterator itr = parameters.iterator(); itr.hasNext(); ) { - ParameterValue v = itr.next(); - if (!names.contains(v.getName())) - r.add(v); + for (ParameterValue v : parameters) { + if (v == null) continue; + if (!names.contains(v.getName())) { + combinedParameters.add(v); + } } - r.addAll(newValues); + return new ParametersAction(combinedParameters); + } - return new ParametersAction(r); + /* + * Creates a new {@link ParametersAction} that contains all the parameters in this action + * with the overrides / new values given as another {@link ParametersAction}. + * @return New {@link ParametersAction}. The result may contain null {@link ParameterValue}s + */ + @Nonnull + public ParametersAction merge(@CheckForNull ParametersAction overrides) { + if (overrides == null) { + return new ParametersAction(parameters); + } + return createUpdated(overrides.getParameters()); } private Object readResolve() { diff --git a/core/src/main/java/hudson/model/ParametersDefinitionProperty.java b/core/src/main/java/hudson/model/ParametersDefinitionProperty.java index 1081078ec9969f4243a6904f41351bf15e7ba6dd..13a6156b5c88a26d7ea23dd2f9a064f4fe0dd92d 100644 --- a/core/src/main/java/hudson/model/ParametersDefinitionProperty.java +++ b/core/src/main/java/hudson/model/ParametersDefinitionProperty.java @@ -24,44 +24,43 @@ */ package hudson.model; +import hudson.Extension; +import hudson.Util; +import hudson.model.Queue.WaitingItem; import java.io.IOException; +import java.util.AbstractList; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.AbstractList; - +import javax.annotation.CheckForNull; import javax.servlet.ServletException; - -import hudson.Util; -import hudson.model.Queue.WaitingItem; +import static javax.servlet.http.HttpServletResponse.SC_CREATED; import jenkins.model.Jenkins; +import jenkins.model.ParameterizedJobMixIn; import jenkins.util.TimeDuration; import net.sf.json.JSONArray; import net.sf.json.JSONObject; - +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; -import hudson.Extension; -import javax.annotation.CheckForNull; -import org.kohsuke.stapler.export.Flavor; - -import static javax.servlet.http.HttpServletResponse.SC_CREATED; - /** * Keeps a list of the parameters defined for a project. * *

* This class also implements {@link Action} so that index.jelly provides - * a form to enter build parameters. + * a form to enter build parameters. + *

The owning job needs a {@code sidepanel.jelly} and should have web methods delegating to {@link ParameterizedJobMixIn#doBuild} and {@link ParameterizedJobMixIn#doBuildWithParameters}. + * The builds also need a {@code sidepanel.jelly}. */ @ExportedBean(defaultVisibility=2) -public class ParametersDefinitionProperty extends JobProperty> +public class ParametersDefinitionProperty extends JobProperty> implements Action { private final List parameterDefinitions; @@ -73,9 +72,15 @@ public class ParametersDefinitionProperty extends JobProperty getOwner() { - return owner; + return (AbstractProject) owner; + } + + @Restricted(NoExternalUse.class) // Jelly + public ParameterizedJobMixIn.ParameterizedJob getJob() { + return (ParameterizedJobMixIn.ParameterizedJob) owner; } @Exported @@ -99,10 +104,16 @@ public class ParametersDefinitionProperty extends JobProperty getJobActions(AbstractProject job) { + public Collection getJobActions(Job job) { return Collections.singleton(this); } + @Deprecated + public Collection getJobActions(AbstractProject job) { + return getJobActions((Job) job); + } + + @Deprecated public AbstractProject getProject() { return (AbstractProject) owner; } @@ -110,17 +121,17 @@ public class ParametersDefinitionProperty extends JobProperty - * This method is supposed to be invoked from {@link AbstractProject#doBuild(StaplerRequest, StaplerResponse, TimeDuration)}. + * This method is supposed to be invoked from {@link ParameterizedJobMixIn#doBuild(StaplerRequest, StaplerResponse, TimeDuration)}. */ public void _doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException { - if (delay==null) delay=new TimeDuration(owner.getQuietPeriod()); + if (delay==null) delay=new TimeDuration(getJob().getQuietPeriod()); List values = new ArrayList(); @@ -136,11 +147,15 @@ public class ParametersDefinitionProperty extends JobProperty jobType) { - return AbstractProject.class.isAssignableFrom(jobType); + return ParameterizedJobMixIn.ParameterizedJob.class.isAssignableFrom(jobType); } @Override diff --git a/core/src/main/java/hudson/model/PasswordParameterDefinition.java b/core/src/main/java/hudson/model/PasswordParameterDefinition.java index 6c3388cd5e6dc062845e54324cb6173df2f531e8..ce93144a56c708d07ebcb519cdb6dbc3e5ae85dc 100644 --- a/core/src/main/java/hudson/model/PasswordParameterDefinition.java +++ b/core/src/main/java/hudson/model/PasswordParameterDefinition.java @@ -28,6 +28,8 @@ import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.DataBoundConstructor; import hudson.Extension; import hudson.util.Secret; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; /** * Parameter whose value is a {@link Secret} and is hidden from the UI. @@ -76,6 +78,11 @@ public class PasswordParameterDefinition extends SimpleParameterDefinition { return Secret.toString(defaultValue); } + @Restricted(DoNotUse.class) // used from Jelly + public Secret getDefaultValueAsSecret() { + return defaultValue; + } + // kept for backward compatibility public void setDefaultValue(String defaultValue) { this.defaultValue = Secret.fromString(defaultValue); diff --git a/core/src/main/java/hudson/model/PasswordParameterValue.java b/core/src/main/java/hudson/model/PasswordParameterValue.java index 8cbdda50a77e0fa237cd513cf13bb019a33b54cc..d7d917ee487adf7bb96764f2c4dd2e4841a116d9 100644 --- a/core/src/main/java/hudson/model/PasswordParameterValue.java +++ b/core/src/main/java/hudson/model/PasswordParameterValue.java @@ -49,7 +49,7 @@ public class PasswordParameterValue extends ParameterValue { } @Override - public void buildEnvVars(AbstractBuild build, EnvVars env) { + public void buildEnvironment(Run build, EnvVars env) { String v = Secret.toString(value); env.put(name, v); env.put(name.toUpperCase(Locale.ENGLISH),v); // backward compatibility pre 1.345 diff --git a/core/src/main/java/hudson/model/PeriodicWork.java b/core/src/main/java/hudson/model/PeriodicWork.java index 9bb02abd3a4fb4a3f884a597ac6f3f7723929a2c..c3f593df2305b43dd53b0a600515ec83d5aececd 100644 --- a/core/src/main/java/hudson/model/PeriodicWork.java +++ b/core/src/main/java/hudson/model/PeriodicWork.java @@ -23,16 +23,20 @@ */ package hudson.model; +import hudson.init.Initializer; import hudson.triggers.SafeTimerTask; import hudson.triggers.Trigger; import hudson.ExtensionPoint; import hudson.Extension; import hudson.ExtensionList; import jenkins.model.Jenkins; +import jenkins.util.Timer; +import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import java.util.Random; -import java.util.Timer; + +import static hudson.init.InitMilestone.JOB_LOADED; /** * Extension point to perform a periodic task in Hudson (through {@link Timer}.) @@ -53,6 +57,9 @@ import java.util.Timer; * @see AsyncPeriodicWork */ public abstract class PeriodicWork extends SafeTimerTask implements ExtensionPoint { + + /** @deprecated Use your own logger, or send messages to the logger in {@link AsyncPeriodicWork#execute}. */ + @SuppressWarnings("NonConstantLogger") protected final Logger logger = Logger.getLogger(getClass().getName()); /** @@ -89,6 +96,14 @@ public abstract class PeriodicWork extends SafeTimerTask implements ExtensionPoi return Jenkins.getInstance().getExtensionList(PeriodicWork.class); } + @Initializer(after= JOB_LOADED) + public static void init() { + // start all PeriodicWorks + for (PeriodicWork p : PeriodicWork.all()) { + Timer.get().scheduleAtFixedRate(p, p.getInitialDelay(), p.getRecurrencePeriod(), TimeUnit.MILLISECONDS); + } + } + // time constants protected static final long MIN = 1000*60; protected static final long HOUR =60*MIN; diff --git a/core/src/main/java/hudson/model/Project.java b/core/src/main/java/hudson/model/Project.java index 13127ccd2e2794f359c332a1d4b14ceecc55c1af..701ed893f7df0b5092fc63e187d4e730768308a4 100644 --- a/core/src/main/java/hudson/model/Project.java +++ b/core/src/main/java/hudson/model/Project.java @@ -26,6 +26,8 @@ package hudson.model; import hudson.Util; import hudson.model.Descriptor.FormException; +import hudson.model.queue.QueueTaskFuture; +import hudson.scm.SCM; import hudson.tasks.BuildStep; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrappers; @@ -35,6 +37,7 @@ import hudson.tasks.Publisher; import hudson.tasks.Maven; import hudson.tasks.Maven.ProjectWithMaven; import hudson.tasks.Maven.MavenInstallation; +import hudson.triggers.SCMTrigger; import hudson.triggers.Trigger; import hudson.util.DescribableList; import net.sf.json.JSONObject; @@ -43,11 +46,13 @@ import org.kohsuke.stapler.StaplerResponse; import javax.servlet.ServletException; import java.io.IOException; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import jenkins.triggers.SCMTriggerItem; /** * Buildable software project. @@ -55,7 +60,7 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; * @author Kohsuke Kawaguchi */ public abstract class Project

,B extends Build> - extends AbstractProject implements SCMedItem, Saveable, ProjectWithMaven, BuildableItemWithBuildWrappers { + extends AbstractProject implements SCMTriggerItem, Saveable, ProjectWithMaven, BuildableItemWithBuildWrappers { /** * List of active {@link Builder}s configured for this project. @@ -97,6 +102,22 @@ public abstract class Project

,B extends Build> return this; } + @Override public Item asItem() { + return this; + } + + @Override public QueueTaskFuture scheduleBuild2(int quietPeriod, Action... actions) { + return scheduleBuild2(quietPeriod, null, actions); + } + + @Override public SCMTrigger getSCMTrigger() { + return getTrigger(SCMTrigger.class); + } + + @Override public Collection getSCMs() { + return SCMTriggerItem.SCMTriggerItems.resolveMultiScmIfConfigured(getScm()); + } + public List getBuilders() { return getBuildersList().toList(); } @@ -175,7 +196,8 @@ public abstract class Project

,B extends Build> return null; } - protected void buildDependencyGraph(DependencyGraph graph) { + @Override protected void buildDependencyGraph(DependencyGraph graph) { + super.buildDependencyGraph(graph); getPublishersList().buildDependencyGraph(this,graph); getBuildersList().buildDependencyGraph(this,graph); getBuildWrappersList().buildDependencyGraph(this,graph); diff --git a/core/src/main/java/hudson/model/ProxyView.java b/core/src/main/java/hudson/model/ProxyView.java index 3c3c3c3f637ca7d8ac9c5f7af7979ba384012679..d8599ce9c2b8f832d73520091ff6ce70986b2e9d 100644 --- a/core/src/main/java/hudson/model/ProxyView.java +++ b/core/src/main/java/hudson/model/ProxyView.java @@ -38,6 +38,7 @@ import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerFallback; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.interceptor.RequirePOST; /** * A view that delegates to another. @@ -89,13 +90,6 @@ public class ProxyView extends View implements StaplerFallback { return getProxiedView().contains(item); } - @Override - public void onJobRenamed(Item item, String oldName, String newName) { - if (oldName.equals(proxiedViewName)) { - proxiedViewName = newName; - } - } - @Override protected void submit(StaplerRequest req) throws IOException, ServletException, FormException { String proxiedViewName = req.getSubmittedForm().getString("proxiedViewName"); @@ -105,6 +99,7 @@ public class ProxyView extends View implements StaplerFallback { this.proxiedViewName = proxiedViewName; } + @RequirePOST @Override public Item doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { return getProxiedView().doCreateItem(req, rsp); diff --git a/core/src/main/java/hudson/model/Queue.java b/core/src/main/java/hudson/model/Queue.java index 81999ef2c34f31ad3d87630d113e0562d655029a..f95a496c52d740e604b3c13a891262a1f3361f1d 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -28,7 +28,6 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableList; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; -import hudson.AbortException; import hudson.BulkChange; import hudson.CopyOnWrite; import hudson.ExtensionList; @@ -66,9 +65,8 @@ import hudson.model.queue.CauseOfBlockage.BecauseLabelIsOffline; import hudson.model.queue.CauseOfBlockage.BecauseNodeIsBusy; import hudson.model.queue.WorkUnitContext; import hudson.security.ACL; +import jenkins.util.Timer; import hudson.triggers.SafeTimerTask; -import hudson.triggers.Trigger; -import hudson.util.OneShotEvent; import hudson.util.TimeUnit2; import hudson.util.XStream2; import hudson.util.ConsistentHash; @@ -93,9 +91,8 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; -import java.util.Timer; import java.util.TreeSet; -import java.util.Map.Entry; +import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.Future; @@ -109,6 +106,7 @@ import javax.servlet.ServletException; import jenkins.model.Jenkins; import jenkins.security.QueueItemAuthenticator; import jenkins.security.QueueItemAuthenticatorConfiguration; +import jenkins.util.AtmostOneTaskExecutor; import org.acegisecurity.AccessDeniedException; import org.acegisecurity.Authentication; import org.kohsuke.stapler.HttpResponse; @@ -116,8 +114,12 @@ import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; + import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.basic.AbstractSingleValueConverter; +import javax.annotation.CheckForNull; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.interceptor.RequirePOST; @@ -227,22 +229,15 @@ public class Queue extends ResourceController implements Saveable { * This is a job offer from the queue to an executor. * *

- * An idle executor (that calls {@link Queue#pop()} creates - * a new {@link JobOffer} and gets itself {@linkplain Queue#parked parked}, - * and we'll eventually hand out an {@link #workUnit} to build. + * For each idle executor, this gets created to allow the scheduling logic + * to assign a work. Once a work is assigned, the executor actually gets + * started to carry out the task in question. */ public class JobOffer extends MappingWorksheet.ExecutorSlot { public final Executor executor; - /** - * Used to wake up an executor, when it has an offered - * {@link Project} to build. - */ - private final OneShotEvent event = new OneShotEvent(Queue.this); - /** * The work unit that this {@link Executor} is going to handle. - * (Or null, in which case event is used to trigger a queue maintenance.) */ private WorkUnit workUnit; @@ -254,7 +249,9 @@ public class Queue extends ResourceController implements Saveable { protected void set(WorkUnit p) { assert this.workUnit == null; this.workUnit = p; - event.signal(); + assert executor.isParking(); + executor.start(workUnit); + // LOGGER.info("Starting "+executor.getName()); } @Override @@ -286,6 +283,7 @@ public class Queue extends ResourceController implements Saveable { return workUnit == null && !executor.getOwner().isOffline() && executor.getOwner().isAcceptingTasks(); } + @CheckForNull public Node getNode() { return executor.getOwner().getNode(); } @@ -300,20 +298,23 @@ public class Queue extends ResourceController implements Saveable { } } - /** - * The executors that are currently waiting for a job to run. - */ - private final Map parked = new HashMap(); - private volatile transient LoadBalancer loadBalancer; private volatile transient QueueSorter sorter; + private transient final AtmostOneTaskExecutor maintainerThread = new AtmostOneTaskExecutor(new Callable() { + @Override + public Void call() throws Exception { + maintain(); + return null; + } + }); + public Queue(LoadBalancer loadBalancer) { this.loadBalancer = loadBalancer.sanitize(); // if all the executors are busy doing something, then the queue won't be maintained in // timely fashion, so use another thread to make sure it happens. - new MaintainTask(this); + new MaintainTask(this).periodic(); } public LoadBalancer getLoadBalancer() { @@ -452,12 +453,9 @@ public class Queue extends ResourceController implements Saveable { /** * Schedule a new build for this project. - * - * @return true if the project is actually added to the queue. - * false if the queue contained it and therefore the add() - * was noop + * @see #schedule(Task, int) */ - public WaitingItem schedule(AbstractProject p) { + public @CheckForNull WaitingItem schedule(AbstractProject p) { return schedule(p, p.getQuietPeriod()); } @@ -543,7 +541,7 @@ public class Queue extends ResourceController implements Saveable { shouldScheduleItem |= action.shouldSchedule(actions); } for (QueueAction action: Util.filter(actions,QueueAction.class)) { - shouldScheduleItem |= action.shouldSchedule(item.getActions()); + shouldScheduleItem |= action.shouldSchedule((new ArrayList(item.getAllActions()))); } if(!shouldScheduleItem) { duplicatesInQueue.add(item); @@ -607,7 +605,7 @@ public class Queue extends ResourceController implements Saveable { return schedule(p, quietPeriod)!=null; } - public synchronized WaitingItem schedule(Task p, int quietPeriod) { + public synchronized @CheckForNull WaitingItem schedule(Task p, int quietPeriod) { return schedule(p, quietPeriod, new Action[0]); } @@ -622,7 +620,7 @@ public class Queue extends ResourceController implements Saveable { /** * Convenience wrapper method around {@link #schedule(Task, int, List)} */ - public synchronized WaitingItem schedule(Task p, int quietPeriod, Action... actions) { + public synchronized @CheckForNull WaitingItem schedule(Task p, int quietPeriod, Action... actions) { return schedule2(p, quietPeriod, actions).getCreateItem(); } @@ -652,7 +650,7 @@ public class Queue extends ResourceController implements Saveable { public synchronized boolean cancel(Item item) { LOGGER.log(Level.FINE, "Cancelling {0} item#{1}", new Object[] {item.task, item.id}); - boolean r = item.leave(this); + boolean r = item.cancel(this); LeftItem li = new LeftItem(item); li.enter(this); @@ -742,6 +740,8 @@ public class Queue extends ResourceController implements Saveable { private void _getBuildableItems(Computer c, ItemList col, List result) { Node node = c.getNode(); + if (node == null) // Deleted computers cannot take build items... + return; for (BuildableItem p : col.values()) { if (node.canTake(p) == null) result.add(p); @@ -825,11 +825,13 @@ public class Queue extends ResourceController implements Saveable { public synchronized int countBuildableItemsFor(Label l) { int r = 0; for (BuildableItem bi : buildables.values()) - if(bi.getAssignedLabel()==l) - r++; + for (SubTask st : bi.task.getSubTasks()) + if (null==l || bi.getAssignedLabelFor(st)==l) + r++; for (BuildableItem bi : pendings.values()) - if(bi.getAssignedLabel()==l) - r++; + for (SubTask st : bi.task.getSubTasks()) + if (null==l || bi.getAssignedLabelFor(st)==l) + r++; return r; } @@ -837,7 +839,7 @@ public class Queue extends ResourceController implements Saveable { * Counts all the {@link BuildableItem}s currently in the queue. */ public synchronized int countBuildableItems() { - return buildables.size()+pendings.size(); + return countBuildableItemsFor(null); } /** @@ -857,8 +859,9 @@ public class Queue extends ResourceController implements Saveable { return bi; for (Item item : waitingList) { - if (item.task == t) + if (item.task.equals(t)) { return item; + } } return null; } @@ -874,8 +877,9 @@ public class Queue extends ResourceController implements Saveable { result.addAll(buildables.getAll(t)); result.addAll(pendings.getAll(t)); for (Item item : waitingList) { - if (item.task == t) + if (item.task.equals(t)) { result.add(item); + } } return result; } @@ -896,101 +900,24 @@ public class Queue extends ResourceController implements Saveable { if (blockedProjects.containsKey(t) || buildables.containsKey(t) || pendings.containsKey(t)) return true; for (Item item : waitingList) { - if (item.task == t) + if (item.task.equals(t)) { return true; + } } return false; } /** - * Called by the executor to fetch something to build next. - *

- * This method blocks until a next project becomes buildable. + * Called when the executor actually starts executing the assigned work unit. + * + * This moves the task from the pending state to the "left the queue" state. */ - public synchronized WorkUnit pop() throws InterruptedException { - final Executor exec = Executor.currentExecutor(); + /*package*/ synchronized void onStartExecuting(Executor exec) throws InterruptedException { + final WorkUnit wu = exec.getCurrentWorkUnit(); + pendings.remove(wu.context.item); - if (exec instanceof OneOffExecutor) { - OneOffExecutor ooe = (OneOffExecutor) exec; - final WorkUnit wu = ooe.getAssignedWorkUnit(); - pendings.remove(wu.context.item); - - LeftItem li = new LeftItem(wu.context); - li.enter(this); - return wu; - } - - try { - while (true) { - final JobOffer offer = new JobOffer(exec); - long sleep = -1; - - // consider myself parked - assert !parked.containsKey(exec); - parked.put(exec, offer); - - // reuse executor thread to do a queue maintenance. - // at the end of this we get all the buildable jobs - // in the buildables field. - maintain(); - - // we went over all the buildable projects and awaken - // all the executors that got work to do. now, go to sleep - // until this thread is awakened. If this executor assigned a job to - // itself above, the block method will return immediately. - - if (!waitingList.isEmpty()) { - // wait until the first item in the queue is due - sleep = peek().timestamp.getTimeInMillis() - new GregorianCalendar().getTimeInMillis(); - if (sleep < 100) sleep = 100; // avoid wait(0) - } - - if (sleep == -1) - offer.event.block(); - else - offer.event.block(sleep); - - // retract the offer object - assert parked.get(exec) == offer; - parked.remove(exec); - - // am I woken up because I have a project to build? - if (offer.workUnit != null) { - // if so, just build it - LOGGER.log(Level.FINE, "Pop returning {0} for {1}", new Object[] {offer.workUnit, exec.getName()}); - - // TODO: I think this has to be done by the last executor that leaves the pop(), not by main executor - if (offer.workUnit.isMainWork()) { - pendings.remove(offer.workUnit.context.item); - LeftItem li = new LeftItem(offer.workUnit.context); - li.enter(this); - } - - return offer.workUnit; - } - // otherwise run a queue maintenance - } - } finally { - // remove myself from the parked list - JobOffer offer = parked.remove(exec); - if (offer != null && offer.workUnit != null) { - // we are already assigned a project, but now we can't handle it. - offer.workUnit.context.abort(new AbortException()); - if(offer.workUnit.context.item!=null && pendings.contains(offer.workUnit.context.item)){ - //we are already assigned a project and moved it into pendings, but something wrong had happened before an executor could take it. - pendings.remove(offer.workUnit.context.item); - //return it into queue, it does not have to cause this problem, it can be caused by another item. - buildables.add(offer.workUnit.context.item); - } - - } - - // since this executor might have been chosen for - // maintenance, schedule another one. Worst case - // we'll just run a pointless maintenance, and that's - // fine. - scheduleMaintenance(); - } + LeftItem li = new LeftItem(wu.context); + li.enter(this); } /** @@ -1001,16 +928,10 @@ public class Queue extends ResourceController implements Saveable { *

* This wakes up one {@link Executor} so that it will maintain a queue. */ - public synchronized void scheduleMaintenance() { - // this code assumes that after this method is called - // no more executors will be offered job except by - // the pop() code. - for (Entry av : parked.entrySet()) { - if (av.getValue().workUnit == null) { - av.getValue().event.signal(); - return; - } - } + @WithBridgeMethods(void.class) + public Future scheduleMaintenance() { + // LOGGER.info("Scheduling maintenance"); + return maintainerThread.submit(); } /** @@ -1058,6 +979,20 @@ public class Queue extends ResourceController implements Saveable { public synchronized void maintain() { LOGGER.log(Level.FINE, "Queue maintenance started {0}", this); + // The executors that are currently waiting for a job to run. + Map parked = new HashMap(); + + {// update parked + for (Computer c : Jenkins.getInstance().getComputers()) { + for (Executor e : c.getExecutors()) { + if (e.isParking()) { + parked.put(e,new JobOffer(e)); + } + } + } + } + + {// blocked -> buildable for (BlockedItem p : new ArrayList(blockedProjects.values())) {// copy as we'll mutate the list if (!isBuildBlocked(p) && allowNewBuildableTask(p.task)) { @@ -1072,7 +1007,7 @@ public class Queue extends ResourceController implements Saveable { while (!waitingList.isEmpty()) { WaitingItem top = peek(); - if (!top.timestamp.before(new GregorianCalendar())) + if (top.timestamp.compareTo(new GregorianCalendar())>0) break; // finished moving all ready items from queue top.leave(this); @@ -1159,6 +1094,7 @@ public class Queue extends ResourceController implements Saveable { } private boolean makePending(BuildableItem p) { + // LOGGER.info("Making "+p.task+" pending"); // REMOVE p.isPending = true; return pendings.add(p); } @@ -1342,14 +1278,13 @@ public class Queue extends ResourceController implements Saveable { public interface Executable extends Runnable { /** * Task from which this executable was created. - * Never null. * *

* Since this method went through a signature change in 1.377, the invocation may results in * {@link AbstractMethodError}. * Use {@link Executables#getParentOf(Queue.Executable)} that avoids this. */ - SubTask getParent(); + @Nonnull SubTask getParent(); /** * Called by {@link Executor} to perform the task @@ -1459,6 +1394,21 @@ public class Queue extends ResourceController implements Saveable { } return task.getAssignedLabel(); } + + /** + * Test if the specified {@link SubTask} needs to be run on a node with a particular label, and + * return that {@link Label}. Otherwise null, indicating it can run on anywhere. + * + *

+ * This code takes {@link LabelAssignmentAction} into account, then falls back to {@link SubTask#getAssignedLabel()} + */ + public Label getAssignedLabelFor(SubTask st) { + for (LabelAssignmentAction laa : getActions(LabelAssignmentAction.class)) { + Label l = laa.getAssignedLabel(st); + if (l!=null) return l; + } + return st.getAssignedLabel(); + } /** * Convenience method that returns a read only view of the {@link Cause}s associated with this item in the queue. @@ -1473,6 +1423,16 @@ public class Queue extends ResourceController implements Saveable { return Collections.emptyList(); } + @Restricted(DoNotUse.class) // used from Jelly + public String getCausesDescription() { + List causes = getCauses(); + StringBuilder s = new StringBuilder(); + for (Cause c : causes) { + s.append(c.getShortDescription()).append('\n'); + } + return s.toString(); + } + protected Item(Task task, List actions, int id, FutureImpl future) { this.task = task; this.id = id; @@ -1490,7 +1450,7 @@ public class Queue extends ResourceController implements Saveable { } protected Item(Item item) { - this(item.task, item.getActions(), item.id, item.future, item.inQueueSince); + this(item.task, new ArrayList(item.getAllActions()), item.id, item.future, item.inQueueSince); } /** @@ -1526,13 +1486,10 @@ public class Queue extends ResourceController implements Saveable { @Exported public String getParams() { StringBuilder s = new StringBuilder(); - for(Action action : getActions()) { - if(action instanceof ParametersAction) { - ParametersAction pa = (ParametersAction)action; - for (ParameterValue p : pa.getParameters()) { - s.append('\n').append(p.getShortDescription()); - } - } + for (ParametersAction pa : getActions(ParametersAction.class)) { + for (ParameterValue p : pa.getParameters()) { + s.append('\n').append(p.getShortDescription()); + } } return s.toString(); } @@ -1591,7 +1548,7 @@ public class Queue extends ResourceController implements Saveable { @Override public String toString() { - return getClass().getName() + ':' + task + ':' + getWhy(); + return getClass().getName() + ':' + task + ':' + id; } /** @@ -1844,26 +1801,10 @@ public class Queue extends ResourceController implements Saveable { else return new BecauseNodeIsBusy(nodes.iterator().next()); } } else { - CauseOfBlockage c; - for (Node node : allNodes) { - if (node.toComputer().isPartiallyIdle()) { - c = canTake(node); - if (c==null) break; - } - } - return CauseOfBlockage.createNeedsMoreExecutor(Messages._Queue_WaitingForNextAvailableExecutor()); } } - private CauseOfBlockage canTake(Node node) { - for (QueueTaskDispatcher d : QueueTaskDispatcher.all()) { - CauseOfBlockage cause = d.canTake(node, this); - if (cause!=null) return cause; - } - return null; - } - @Override public boolean isStuck() { Label label = getAssignedLabel(); @@ -2070,12 +2011,11 @@ public class Queue extends ResourceController implements Saveable { MaintainTask(Queue queue) { this.queue = new WeakReference(queue); + } + private void periodic() { long interval = 5000; - Timer timer = Trigger.timer; - if (timer != null) { - timer.schedule(this, interval, interval); - } + Timer.get().scheduleWithFixedDelay(this, interval, interval, TimeUnit.MILLISECONDS); } protected void doRun() { @@ -2093,7 +2033,7 @@ public class Queue extends ResourceController implements Saveable { private class ItemList extends ArrayList { public T get(Task task) { for (T item: this) { - if (item.task == task) { + if (item.task.equals(task)) { return item; } } @@ -2103,7 +2043,7 @@ public class Queue extends ResourceController implements Saveable { public List getAll(Task task) { List result = new ArrayList(); for (T item: this) { - if (item.task == task) { + if (item.task.equals(task)) { result.add(item); } } @@ -2118,7 +2058,7 @@ public class Queue extends ResourceController implements Saveable { Iterator it = iterator(); while (it.hasNext()) { T t = it.next(); - if (t.task == task) { + if (t.task.equals(task)) { it.remove(); return t; } @@ -2127,7 +2067,7 @@ public class Queue extends ResourceController implements Saveable { } public void put(Task task, T item) { - assert item.task == task; + assert item.task.equals(task); add(item); } diff --git a/core/src/main/java/hudson/model/ReconfigurableDescribable.java b/core/src/main/java/hudson/model/ReconfigurableDescribable.java index 7738089b1d8bc1236c380df1ec36067d16dbef30..e9dd8c2f6c01ffadcccdd6f37fdfa5f9afacf17c 100644 --- a/core/src/main/java/hudson/model/ReconfigurableDescribable.java +++ b/core/src/main/java/hudson/model/ReconfigurableDescribable.java @@ -25,6 +25,8 @@ package hudson.model; import hudson.model.Descriptor.FormException; import hudson.slaves.NodeProperty; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import net.sf.json.JSONObject; import org.kohsuke.stapler.StaplerRequest; @@ -77,5 +79,5 @@ public interface ReconfigurableDescribable * If the activity doesn't lock any resources, just - * return {@code new ResourceList()}. + * return {@link ResourceList#EMPTY}. * * @return never null */ diff --git a/core/src/main/java/hudson/model/Result.java b/core/src/main/java/hudson/model/Result.java index cd6226a9c757b1ab679929f1e29dc25825da7f0a..b17a98a4cf5b11d45b5ea56ac6d83ed2b4e1c656 100644 --- a/core/src/main/java/hudson/model/Result.java +++ b/core/src/main/java/hudson/model/Result.java @@ -26,6 +26,7 @@ package hudson.model; import com.thoughtworks.xstream.converters.SingleValueConverter; import com.thoughtworks.xstream.converters.basic.AbstractSingleValueConverter; import hudson.cli.declarative.OptionHandlerExtension; +import hudson.init.Initializer; import hudson.util.EditDistance; import org.apache.commons.beanutils.Converter; import org.kohsuke.args4j.CmdLineException; @@ -201,7 +202,8 @@ public final class Result implements Serializable, CustomExportedBean { } } - static { + @Initializer + public static void init() { Stapler.CONVERT_UTILS.register(new Converter() { public Object convert(Class type, Object value) { return Result.fromString(value.toString()); diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index 965140183466ab74043e43895b2c718357bd9e87..377b5d5ae3df7f6890992ec51a6fd4aee6139ba2 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -27,46 +27,50 @@ */ package hudson.model; -import hudson.console.ConsoleLogFilter; -import hudson.Functions; +import com.jcraft.jzlib.GZIPInputStream; +import com.thoughtworks.xstream.XStream; import hudson.AbortException; import hudson.BulkChange; import hudson.EnvVars; import hudson.ExtensionPoint; import hudson.FeedAdapter; -import hudson.FilePath; +import hudson.Functions; import hudson.Util; import hudson.XmlFile; import hudson.cli.declarative.CLIMethod; import hudson.console.AnnotatedLargeText; +import hudson.console.ConsoleLogFilter; import hudson.console.ConsoleNote; -import hudson.matrix.MatrixBuild; -import hudson.matrix.MatrixRun; +import hudson.console.ModelHyperlinkNote; import hudson.model.Descriptor.FormException; +import hudson.model.Run.RunExecution; import hudson.model.listeners.RunListener; import hudson.model.listeners.SaveableListener; -import hudson.security.PermissionScope; import hudson.search.SearchIndexBuilder; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.Permission; import hudson.security.PermissionGroup; +import hudson.security.PermissionScope; import hudson.tasks.BuildWrapper; import hudson.tasks.test.AbstractTestResultAction; import hudson.util.FlushProofOutputStream; import hudson.util.FormApply; -import hudson.util.IOException2; import hudson.util.LogTaskListener; +import hudson.util.ProcessTree; import hudson.util.XStream2; - import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.io.PrintWriter; import java.io.Reader; +import java.io.StringWriter; import java.io.Writer; import java.nio.charset.Charset; import java.text.DateFormat; @@ -80,25 +84,35 @@ import java.util.Comparator; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.HashSet; import java.util.logging.Level; +import static java.util.logging.Level.*; import java.util.logging.Logger; -import com.jcraft.jzlib.GZIPInputStream; - +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; - +import jenkins.model.ArtifactManager; +import jenkins.model.ArtifactManagerConfiguration; +import jenkins.model.ArtifactManagerFactory; import jenkins.model.BuildDiscarder; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; +import jenkins.model.PeepholePermalink; +import jenkins.model.RunAction2; +import jenkins.model.StandardArtifactManager; +import jenkins.model.lazy.BuildReference; +import jenkins.util.VirtualFile; import jenkins.util.io.OnMaster; import net.sf.json.JSONObject; +import org.acegisecurity.AccessDeniedException; +import org.acegisecurity.Authentication; import org.apache.commons.io.IOUtils; import org.apache.commons.jelly.XMLOutput; import org.kohsuke.accmod.Restricted; @@ -106,27 +120,8 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.*; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; - -import com.thoughtworks.xstream.XStream; -import hudson.model.Run.RunExecution; -import java.io.ByteArrayInputStream; import org.kohsuke.stapler.interceptor.RequirePOST; -import java.io.FileOutputStream; -import java.io.OutputStream; - -import java.io.StringWriter; -import static java.util.logging.Level.*; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import jenkins.model.ArtifactManager; -import jenkins.model.ArtifactManagerConfiguration; -import jenkins.model.ArtifactManagerFactory; -import jenkins.model.PeepholePermalink; -import jenkins.model.StandardArtifactManager; -import jenkins.model.RunAction2; -import jenkins.util.VirtualFile; - /** * A particular execution of {@link Job}. * @@ -142,7 +137,7 @@ import jenkins.util.VirtualFile; public abstract class Run ,RunT extends Run> extends Actionable implements ExtensionPoint, Comparable, AccessControlled, PersistenceRoot, DescriptorByNameOwner, OnMaster { - protected transient final JobT project; + protected transient final @Nonnull JobT project; /** * Build number. @@ -155,7 +150,7 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run getTransientActions() { List actions = new ArrayList(); for (TransientBuildActionFactory factory: TransientBuildActionFactory.all()) { - actions.addAll(factory.createFor(this)); - assert !actions.contains(null) : "null action added by " + factory; + for (Action created : factory.createFor(this)) { + if (created == null) { + LOGGER.log(WARNING, "null action added by {0}", factory); + continue; + } + actions.add(created); + } } return Collections.unmodifiableList(actions); } - + + /** + * {@inheritDoc} + * A {@link RunAction2} is handled specially. + */ @SuppressWarnings("deprecation") @Override - public void addAction(Action a) { + public void addAction(@Nonnull Action a) { super.addAction(a); if (a instanceof RunAction2) { ((RunAction2) a).onAttached(this); @@ -371,7 +391,8 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run getBadgeActions() { - List r = null; - for (Action a : getActions()) { - if(a instanceof BuildBadgeAction) { - if(r==null) - r = new ArrayList(); - r.add((BuildBadgeAction)a); - } - } + public @Nonnull List getBadgeActions() { + List r = getActions(BuildBadgeAction.class); if(isKeepLog()) { - if(r==null) - r = new ArrayList(); r.add(new KeepLogBuildBadge()); } - if(r==null) return Collections.emptyList(); - else return r; + return r; } /** @@ -465,6 +471,15 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run * If a build sits in the queue for a long time, multiple build requests made during this period @@ -532,7 +547,7 @@ public abstract class Run ,RunT extends Run getCauses() { + public @Nonnull List getCauses() { CauseAction a = getAction(CauseAction.class); if (a==null) return Collections.emptyList(); return Collections.unmodifiableList(a.getCauses()); @@ -542,8 +557,8 @@ public abstract class Run ,RunT extends Run T getCause(Class type) { + */ + public @CheckForNull T getCause(Class type) { for (Cause c : getCauses()) if (type.isInstance(c)) return type.cast(c); @@ -563,8 +578,8 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run createReference() { + return new BuildReference(getId(), _this()); + } + /** * Called by {@link RunMap} to drop bi-directional links in preparation for * deleting a build. + * @see jenkins.model.lazy.LazyBuildMixIn.RunMixIn#dropLinks + * @since 1.556 */ - /*package*/ void dropLinks() { + protected void dropLinks() { if(nextBuild!=null) nextBuild.previousBuild = previousBuild; if(previousBuild!=null) previousBuild.nextBuild = nextBuild; } - public RunT getPreviousBuild() { + /** + * @see jenkins.model.lazy.LazyBuildMixIn.RunMixIn#getPreviousBuild + */ + public @CheckForNull RunT getPreviousBuild() { return previousBuild; } /** * Gets the most recent {@linkplain #isBuilding() completed} build excluding 'this' Run itself. - */ - public final RunT getPreviousCompletedBuild() { + */ + public final @CheckForNull RunT getPreviousCompletedBuild() { RunT r=getPreviousBuild(); while (r!=null && r.isBuilding()) r=r.getPreviousBuild(); @@ -799,8 +829,8 @@ public abstract class Run ,RunT extends Run * We basically follow the existing skip list, and wherever we find a non-optimal pointer, we remember them * in 'fixUp' and update them later. - */ - public final RunT getPreviousBuildInProgress() { + */ + public final @CheckForNull RunT getPreviousBuildInProgress() { if(previousBuildInProgress==this) return null; // the most common case List fixUp = new ArrayList(); @@ -835,8 +865,8 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run getPreviousBuildsOverThreshold(int numberOfBuilds, Result threshold) { + */ + public @Nonnull List getPreviousBuildsOverThreshold(int numberOfBuilds, @Nonnull Result threshold) { List builds = new ArrayList(numberOfBuilds); RunT r = getPreviousBuild(); @@ -900,7 +930,10 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run getArtifacts() { + * @return The list can be empty but never null + */ + @Exported + public @Nonnull List getArtifacts() { return getArtifactsUpTo(Integer.MAX_VALUE); } /** * Gets the first N artifacts. - */ - public List getArtifactsUpTo(int n) { + * @return The list can be empty but never null + */ + public @Nonnull List getArtifactsUpTo(int artifactsNumber) { ArtifactList r = new ArtifactList(); try { - addArtifacts(getArtifactManager().root(), "", "", r, null, n); + addArtifacts(getArtifactManager().root(), "", "", r, null, artifactsNumber); } catch (IOException x) { LOGGER.log(Level.WARNING, null, x); } @@ -1050,16 +1087,17 @@ public abstract class Run ,RunT extends Run + * Check if the {@link Run} contains artifacts. * The strange method name is so that we can access it from EL. + * @return true if this run has any artifacts */ public boolean getHasArtifacts() { return !getArtifactsUpTo(1).isEmpty(); } - private int addArtifacts(VirtualFile dir, String path, String pathHref, ArtifactList r, Artifact parent, int upTo) throws IOException { + private int addArtifacts(@Nonnull VirtualFile dir, + @Nonnull String path, @Nonnull String pathHref, + @Nonnull ArtifactList r, @Nonnull Artifact parent, int upTo) throws IOException { VirtualFile[] kids = dir.list(); Arrays.sort(kids); @@ -1240,10 +1278,10 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run + * The method does not close the {@link OutputStream}. */ - public void writeWholeLogTo(OutputStream out) throws IOException, InterruptedException { + public void writeWholeLogTo(@Nonnull OutputStream out) throws IOException, InterruptedException { long pos = 0; AnnotatedLargeText logText; - do { + logText = getLogText(); + pos = logText.writeLogTo(pos, out); + + while (!logText.isComplete()) { + // Instead of us hitting the log file as many times as possible, instead we get the information once every + // second to avoid CPU usage getting very high. + Thread.sleep(1000); logText = getLogText(); pos = logText.writeLogTo(pos, out); - } while (!logText.isComplete()); + } } /** * Used to URL-bind {@link AnnotatedLargeText}. - */ - public AnnotatedLargeText getLogText() { + * @return A {@link Run} log with annotations + */ + public @Nonnull AnnotatedLargeText getLogText() { return new AnnotatedLargeText(getLogFile(),getCharset(),!isLogUpdated(),this); } @Override - protected SearchIndexBuilder makeSearchIndex() { + protected @Nonnull SearchIndexBuilder makeSearchIndex() { SearchIndexBuilder builder = super.makeSearchIndex() .add("console") .add("changes"); - for (Action a : getActions()) { + for (Action a : getAllActions()) { if(a.getIconFileName()!=null) builder.add(a.getUrlName()); } return builder; } - public Api getApi() { + public @Nonnull Api getApi() { return new Api(this); } - public void checkPermission(Permission p) { + @Override + public void checkPermission(@Nonnull Permission p) { getACL().checkPermission(p); } - public boolean hasPermission(Permission p) { + @Override + public boolean hasPermission(@Nonnull Permission p) { return getACL().hasPermission(p); } + @Override public ACL getACL() { // for now, don't maintain ACL per run, and do it at project level return getParent().getACL(); @@ -1409,9 +1461,11 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run getAttributes() { + public @Nonnull Map getAttributes() { return attributes; } } @@ -1608,11 +1666,11 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run getLog(int maxLines) throws IOException { + public @Nonnull List getLog(int maxLines) throws IOException { int lineCount = 0; List logLines = new LinkedList(); BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(getLogFile()),getCharset())); @@ -1892,7 +1960,7 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run getEnvVars() { + LOGGER.log(WARNING, "deprecated call to Run.getEnvVars\n\tat {0}", new Throwable().getStackTrace()[1]); try { - return getEnvironment(); + return getEnvironment(new LogTaskListener(LOGGER, Level.INFO)); } catch (IOException e) { return new EnvVars(); } catch (InterruptedException e) { @@ -2147,6 +2218,7 @@ public abstract class Run ,RunT extends Run,RunT extends Run - * {@link BuildStep}s that invoke external processes should use this. + * {@link hudson.tasks.BuildStep}s that invoke external processes should use this. * This allows {@link BuildWrapper}s and other project configurations (such as JDK selection) * to take effect. * @@ -2163,10 +2235,10 @@ public abstract class Run ,RunT extends Runnull. + * @return the map with the environmental variables. * @since 1.305 */ - public EnvVars getEnvironment(TaskListener listener) throws IOException, InterruptedException { + public @Nonnull EnvVars getEnvironment(@Nonnull TaskListener listener) throws IOException, InterruptedException { Computer c = Computer.currentComputer(); Node n = c==null ? null : c.getNode(); @@ -2184,7 +2256,7 @@ public abstract class Run ,RunT extends Run,RunT extends Run fromExternalizableId(String id) { + public @Nonnull static Run fromExternalizableId(String id) { int hash = id.lastIndexOf('#'); if (hash <= 0) { throw new IllegalArgumentException("Invalid id"); @@ -2225,7 +2297,7 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run ORDER_BY_DATE = new Comparator() { - public int compare(Run lhs, Run rhs) { + public int compare(@Nonnull Run lhs, @Nonnull Run rhs) { long lt = lhs.getTimeInMillis(); long rt = rhs.getTimeInMillis(); if(lt>rt) return -1; @@ -2296,10 +2366,10 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run> extends AbstractLazyLoadRunMap implements Iterable { /** * Read-only view of this map. @@ -185,8 +182,7 @@ public final class RunMap> extends AbstractLazyLoadRunMap */ @Override protected BuildReference createReference(R r) { - if (r instanceof AbstractBuild) return ((AbstractBuild)r).selfReference; - else return super.createReference(r); + return r.createReference(); } @Override @@ -194,24 +190,21 @@ public final class RunMap> extends AbstractLazyLoadRunMap final SimpleDateFormat formatter = Run.ID_FORMATTER.get(); return new FilenameFilter() { - public boolean accept(File dir, String name) { - // JENKINS-1461 sometimes create bogus data directories with impossible dates, such as year 0, April 31st, - // or August 0th. Date object doesn't roundtrip those, so we eventually fail to load this data. - // Don't even bother trying. - if (!isCorrectDate(name)) { - LOGGER.log(FINE, "Skipping {0}", new File(dir,name)); + @Override public boolean accept(File dir, String name) { + if (name.startsWith("0000")) { + // JENKINS-1461 sometimes create bogus data directories with impossible dates, such as year 0, April 31st, + // or August 0th. Date object doesn't roundtrip those, so we eventually fail to load this data. + // Don't even bother trying. return false; } - return !name.startsWith("0000") && new File(dir,name).isDirectory(); - } - - private boolean isCorrectDate(String name) { try { - if(formatter.format(formatter.parse(name)).equals(name)) + if (formatter.format(formatter.parse(name)).equals(name)) { return true; + } } catch (ParseException e) { // fall through } + LOGGER.log(FINE, "Skipping {0} in {1}", new Object[] {name, dir}); return false; } }; diff --git a/core/src/main/java/hudson/model/RunParameterDefinition.java b/core/src/main/java/hudson/model/RunParameterDefinition.java index bacef92a08c8884f1c2ebd3ca5607b1219291252..a1ad6879e240d57bd5663afc26289fba330313c3 100644 --- a/core/src/main/java/hudson/model/RunParameterDefinition.java +++ b/core/src/main/java/hudson/model/RunParameterDefinition.java @@ -122,11 +122,11 @@ public class RunParameterDefinition extends SimpleParameterDefinition { // use getFilter() method so we dont have to worry about null filter value. switch (getFilter()) { case COMPLETED: - return getProject().getBuilds().overThresholdOnly(Result.ABORTED); + return getProject().getBuilds().overThresholdOnly(Result.ABORTED).completedOnly(); case SUCCESSFUL: - return getProject().getBuilds().overThresholdOnly(Result.UNSTABLE); + return getProject().getBuilds().overThresholdOnly(Result.UNSTABLE).completedOnly(); case STABLE : - return getProject().getBuilds().overThresholdOnly(Result.SUCCESS); + return getProject().getBuilds().overThresholdOnly(Result.SUCCESS).completedOnly(); default: return getProject().getBuilds(); } diff --git a/core/src/main/java/hudson/model/RunParameterValue.java b/core/src/main/java/hudson/model/RunParameterValue.java index 6368a942773322c67cd621d6e3961ab232f853af..6f2f5115397feb28a94a28f3bfbf7930c85a34da 100644 --- a/core/src/main/java/hudson/model/RunParameterValue.java +++ b/core/src/main/java/hudson/model/RunParameterValue.java @@ -23,13 +23,14 @@ */ package hudson.model; -import java.util.Locale; - import hudson.EnvVars; import jenkins.model.Jenkins; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.export.Exported; +import javax.annotation.CheckForNull; +import java.util.Locale; + public class RunParameterValue extends ParameterValue { private final String runId; @@ -37,15 +38,26 @@ public class RunParameterValue extends ParameterValue { @DataBoundConstructor public RunParameterValue(String name, String runId, String description) { super(name, description); - this.runId = runId; + this.runId = check(runId); } public RunParameterValue(String name, String runId) { super(name, null); - this.runId = runId; + this.runId = check(runId); } - public Run getRun() { + private static String check(String runId) { + if (runId == null || runId.indexOf('#') == -1) { + throw new IllegalArgumentException(runId); + } else { + return runId; + } + } + + /** + * Can be null if the {@link Run} that this was pointing to no longer exists. + */ + public @CheckForNull Run getRun() { return Run.fromExternalizableId(runId); } @@ -53,25 +65,42 @@ public class RunParameterValue extends ParameterValue { return runId; } + private String[] split() { + if (runId == null) { + return null; + } + String[] r = runId.split("#"); + if (r.length != 2) { + return null; + } + return r; + } + @Exported public String getJobName() { - return runId.split("#")[0]; + String[] r = split(); + return r == null ? null : r[0]; } @Exported public String getNumber() { - return runId.split("#")[1]; + String[] r = split(); + return r == null ? null : r[1]; + } + + @Override + public Run getValue() { + return getRun(); } - /** * Exposes the name/value as an environment variable. */ @Override - public void buildEnvVars(AbstractBuild build, EnvVars env) { + public void buildEnvironment(Run build, EnvVars env) { Run run = getRun(); - String value = Jenkins.getInstance().getRootUrl() + run.getUrl(); + String value = (null == run) ? "UNKNOWN" : Jenkins.getInstance().getRootUrl() + run.getUrl(); env.put(name, value); env.put(name + ".jobName", getJobName()); // undocumented, left for backward compatibility @@ -80,9 +109,10 @@ public class RunParameterValue extends ParameterValue { env.put(name + ".number" , getNumber ()); // same as above env.put(name + "_NUMBER" , getNumber ()); - env.put(name + "_NAME", run.getDisplayName()); // since 1.504 + // if run is null, default to the standard '#1' display name format + env.put(name + "_NAME", (null == run) ? "#" + getNumber() : run.getDisplayName()); // since 1.504 - String buildResult = (null == run.getResult()) ? "UNKNOWN" : run.getResult().toString(); + String buildResult = (null == run || null == run.getResult()) ? "UNKNOWN" : run.getResult().toString(); env.put(name + "_RESULT", buildResult); // since 1.517 env.put(name.toUpperCase(Locale.ENGLISH),value); // backward compatibility pre 1.345 @@ -95,7 +125,8 @@ public class RunParameterValue extends ParameterValue { } @Override public String getShortDescription() { - return name + "=" + getRun().getFullDisplayName(); + Run run = getRun(); + return name + "=" + ((null == run) ? getJobName() + " #" + getNumber() : run.getFullDisplayName()); } } diff --git a/core/src/main/java/hudson/model/SCMedItem.java b/core/src/main/java/hudson/model/SCMedItem.java index 69f04495084b9913d4e1fc0e70524be6bcd436a6..ef4ae60d94527b3cc5dd285ac72dc92d9d96433e 100644 --- a/core/src/main/java/hudson/model/SCMedItem.java +++ b/core/src/main/java/hudson/model/SCMedItem.java @@ -25,13 +25,10 @@ package hudson.model; import hudson.scm.PollingResult; import hudson.scm.SCM; -import hudson.triggers.SCMTrigger; +import jenkins.triggers.SCMTriggerItem; /** - * {@link Item}s that has associated SCM. - * - * @author Kohsuke Kawaguchi - * @see SCMTrigger + * @deprecated Implement {@link SCMTriggerItem} instead. */ public interface SCMedItem extends BuildableItem { /** diff --git a/core/src/main/java/hudson/model/Slave.java b/core/src/main/java/hudson/model/Slave.java index 46b484959626089c08cb89258bdc91cc6600a4d9..d29ed99a13f2f24fb7b67af3df3924c8ecacc370 100644 --- a/core/src/main/java/hudson/model/Slave.java +++ b/core/src/main/java/hudson/model/Slave.java @@ -56,7 +56,8 @@ import java.util.Set; import javax.servlet.ServletException; -import hudson.util.TimeUnit2; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import jenkins.model.Jenkins; import jenkins.slaves.WorkspaceLocator; @@ -148,7 +149,7 @@ public abstract class Slave extends Node implements Serializable { this(name, nodeDescription, remoteFS, numExecutors, mode, labelString, launcher, retentionStrategy, new ArrayList()); } - public Slave(String name, String nodeDescription, String remoteFS, int numExecutors, + public Slave(@Nonnull String name, String nodeDescription, String remoteFS, int numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, List> nodeProperties) throws FormException, IOException { this.name = name; this.description = nodeDescription; @@ -284,7 +285,7 @@ public abstract class Slave extends Node implements Serializable { * @return * null if not connected. */ - public FilePath getWorkspaceRoot() { + public @CheckForNull FilePath getWorkspaceRoot() { FilePath r = getRootPath(); if(r==null) return null; return r.child(WORKSPACE_ROOT); @@ -342,9 +343,21 @@ public abstract class Slave extends Node implements Serializable { } + /** + * Creates a launcher for the slave. + * + * @return + * If there is no computer it will return a {@link hudson.Launcher.DummyLauncher}, otherwise it + * will return a {@link hudson.Launcher.RemoteLauncher} instead. + */ public Launcher createLauncher(TaskListener listener) { SlaveComputer c = getComputer(); - return new RemoteLauncher(listener, c.getChannel(), c.isUnix()).decorateFor(this); + if (c == null) { + listener.error("Issue with creating launcher for slave " + name + "."); + return new Launcher.DummyLauncher(listener); + } else { + return new RemoteLauncher(listener, c.getChannel(), c.isUnix()).decorateFor(this); + } } /** diff --git a/core/src/main/java/hudson/model/StringParameterValue.java b/core/src/main/java/hudson/model/StringParameterValue.java index a855d409885eec8930ccf38452b4f133a482ffac..f841862237cf10e458dbba79e3134a1bc2d3a8a5 100644 --- a/core/src/main/java/hudson/model/StringParameterValue.java +++ b/core/src/main/java/hudson/model/StringParameterValue.java @@ -52,7 +52,7 @@ public class StringParameterValue extends ParameterValue { * Exposes the name/value as an environment variable. */ @Override - public void buildEnvVars(AbstractBuild build, EnvVars env) { + public void buildEnvironment(Run build, EnvVars env) { env.put(name,value); env.put(name.toUpperCase(Locale.ENGLISH),value); // backward compatibility pre 1.345 } @@ -65,9 +65,13 @@ public class StringParameterValue extends ParameterValue { } }; } - - @Override + @Override + public Object getValue() { + return value; + } + + @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); diff --git a/core/src/main/java/hudson/model/TopLevelItem.java b/core/src/main/java/hudson/model/TopLevelItem.java index 23343eb20988a34be5b3e51a016841f851ececd6..ce362a9b79e30b7b11b929fc628bc17d8ddcbaea 100644 --- a/core/src/main/java/hudson/model/TopLevelItem.java +++ b/core/src/main/java/hudson/model/TopLevelItem.java @@ -25,12 +25,11 @@ package hudson.model; import hudson.Extension; import hudson.ExtensionPoint; -import hudson.matrix.MatrixConfiguration; /** * {@link Item} that can be directly displayed under {@link jenkins.model.Jenkins} or other containers. * (A "container" would be any {@link ItemGroup}{@code }, such as a folder of projects.) - * Ones that don't need to be under specific parent (say, unlike {@link MatrixConfiguration}), + * Ones that don't need to be under specific parent (say, unlike {@code MatrixConfiguration}), * and thus can be freely moved, copied, etc. * *

diff --git a/core/src/main/java/hudson/model/TransientBuildActionFactory.java b/core/src/main/java/hudson/model/TransientBuildActionFactory.java index ec3adf99badf048120e90df56f51c078f7b0559b..414c584b3a4531041af23946fe66b05ad92737b3 100644 --- a/core/src/main/java/hudson/model/TransientBuildActionFactory.java +++ b/core/src/main/java/hudson/model/TransientBuildActionFactory.java @@ -6,6 +6,7 @@ import hudson.ExtensionPoint; import jenkins.model.Jenkins; import java.util.Collection; import java.util.Collections; +import jenkins.model.TransientActionFactory; /** * Extension point for inserting transient {@link Action}s into {@link Run}s. @@ -15,8 +16,9 @@ import java.util.Collections; * @author Lucie Votypkova * @since 1.458 * @see Action + * @deprecated Does not contribute to {@link Run#getActions}. Use {@link TransientActionFactory} instead. */ - +@Deprecated public abstract class TransientBuildActionFactory implements ExtensionPoint { /** * Creates actions for the given build. diff --git a/core/src/main/java/hudson/model/TransientComputerActionFactory.java b/core/src/main/java/hudson/model/TransientComputerActionFactory.java index ee724ce044a8f440a08fb7e02b411475224fd54f..cf4d490eb155bb66bd34321238b69c1e69f0e02d 100644 --- a/core/src/main/java/hudson/model/TransientComputerActionFactory.java +++ b/core/src/main/java/hudson/model/TransientComputerActionFactory.java @@ -30,6 +30,7 @@ import jenkins.model.Jenkins; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import jenkins.model.TransientActionFactory; /** * Extension point for inserting transient {@link hudson.model.Action}s to {@link hudson.model.Computer}s. @@ -39,6 +40,7 @@ import java.util.List; * @author Stephen Connolly * @since 1.405 * @see hudson.model.Action + * @see TransientActionFactory */ public abstract class TransientComputerActionFactory implements ExtensionPoint { /** diff --git a/core/src/main/java/hudson/model/TransientProjectActionFactory.java b/core/src/main/java/hudson/model/TransientProjectActionFactory.java index 9588d83fe9a7c842570685649c55f2c856ea1f94..ebae7d2323e320d83931d59f319c2e91aea1ded1 100644 --- a/core/src/main/java/hudson/model/TransientProjectActionFactory.java +++ b/core/src/main/java/hudson/model/TransientProjectActionFactory.java @@ -30,6 +30,7 @@ import hudson.tasks.BuildStep; import jenkins.model.Jenkins; import java.util.Collection; +import jenkins.model.TransientActionFactory; /** * Extension point for inserting transient {@link Action}s into {@link AbstractProject}s. @@ -50,6 +51,7 @@ import java.util.Collection; * @author Kohsuke Kawaguchi * @since 1.327 * @see Action + * @see TransientActionFactory */ public abstract class TransientProjectActionFactory implements ExtensionPoint { /** diff --git a/core/src/main/java/hudson/model/TreeView.java b/core/src/main/java/hudson/model/TreeView.java index bf16d3d0faad5e218a77306074a50735a468e120..97f6d406c9097b68454cf54e4891efda2b524cb9 100644 --- a/core/src/main/java/hudson/model/TreeView.java +++ b/core/src/main/java/hudson/model/TreeView.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArrayList; +import org.kohsuke.stapler.interceptor.RequirePOST; /** * @@ -106,6 +107,7 @@ public class TreeView extends View implements ViewGroup { // return jobNames.contains(item.getName()); } + @RequirePOST public TopLevelItem doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { ItemGroup ig = getOwnerItemGroup(); if (ig instanceof ModifiableItemGroup) { @@ -119,14 +121,7 @@ public class TreeView extends View implements ViewGroup { return null; } - @Override - public synchronized void onJobRenamed(Item item, String oldName, String newName) { - if(jobNames.remove(oldName) && newName!=null) - jobNames.add(newName); - // forward to children - for (View v : views) - v.onJobRenamed(item,oldName,newName); - } + // TODO listen for changes that might affect jobNames protected void submit(StaplerRequest req) throws IOException, ServletException, FormException { } diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index bb5ad2ccf80b1eb6f9e1f43d32b5e9b7cc07c4ad..f5aecb59ef7bded5c64baf60cb3b28cfca41031f 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -44,7 +44,9 @@ import hudson.security.ACL; import hudson.util.DaemonThreadFactory; import hudson.util.FormValidation; import hudson.util.HttpResponses; +import hudson.util.NamingThreadFactory; import hudson.util.IOException2; +import hudson.util.IOUtils; import hudson.util.PersistedList; import hudson.util.XStream2; import jenkins.RestartRequiredException; @@ -82,7 +84,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import java.util.jar.Attributes; import java.util.jar.JarFile; @@ -131,25 +132,13 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas * @since 1.501 */ private final ExecutorService installerService = new AtmostOneThreadExecutor( - new DaemonThreadFactory(new ThreadFactory() { - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setName("Update center installer thread"); - return t; - } - })); + new NamingThreadFactory(new DaemonThreadFactory(), "Update center installer thread")); /** * An {@link ExecutorService} for updating UpdateSites. */ protected final ExecutorService updateService = Executors.newCachedThreadPool( - new DaemonThreadFactory(new ThreadFactory() { - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setName("Update site data downloader"); - return t; - } - })); + new NamingThreadFactory(new DaemonThreadFactory(), "Update site data downloader")); /** * List of created {@link UpdateCenterJob}s. Access needs to be synchronized. @@ -643,7 +632,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas public List updateAllSites() throws InterruptedException, ExecutionException { List > futures = new ArrayList>(); for (UpdateSite site : getSites()) { - Future future = site.updateDirectly(true); + Future future = site.updateDirectly(DownloadService.signatureCheck); if (future != null) { futures.add(future); } @@ -758,16 +747,19 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas * @see DownloadJob */ public File download(DownloadJob job, URL src) throws IOException { + CountingInputStream in = null; + OutputStream out = null; + URLConnection con = null; try { - URLConnection con = connect(job,src); + con = connect(job,src); int total = con.getContentLength(); - CountingInputStream in = new CountingInputStream(con.getInputStream()); + in = new CountingInputStream(con.getInputStream()); byte[] buf = new byte[8192]; int len; File dst = job.getDestination(); File tmp = new File(dst.getPath()+".tmp"); - OutputStream out = new FileOutputStream(tmp); + out = new FileOutputStream(tmp); LOGGER.info("Downloading "+job.getName()); try { @@ -776,12 +768,9 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas job.status = job.new Installing(total==-1 ? -1 : in.getCount()*100/total); } } catch (IOException e) { - throw new IOException2("Failed to load "+src+" to "+tmp,e); + throw new IOException("Failed to load "+src+" to "+tmp,e); } - in.close(); - out.close(); - if (total!=-1 && total!=tmp.length()) { // don't know exactly how this happens, but report like // http://www.ashlux.com/wordpress/2009/08/14/hudson-and-the-sonar-plugin-fail-maveninstallation-nosuchmethoderror/ @@ -791,7 +780,18 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas return tmp; } catch (IOException e) { - throw new IOException2("Failed to download from "+src,e); + // assist troubleshooting in case of e.g. "too many redirects" by printing actual URL + String extraMessage = ""; + if (con != null && con.getURL() != null && !src.toString().equals(con.getURL().toString())) { + // Two URLs are considered equal if different hosts resolve to same IP. Prefer to log in case of string inequality, + // because who knows how the server responds to different host name in the request header? + // Also, since it involved name resolution, it'd be an expensive operation. + extraMessage = " (redirected to: " + con.getURL() + ")"; + } + throw new IOException2("Failed to download from "+src+extraMessage,e); + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); } } @@ -873,7 +873,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas } catch (SSLHandshakeException e) { if (e.getMessage().contains("PKIX path building failed")) // fix up this crappy error message from JDK - throw new IOException2("Failed to validate the SSL certificate of "+url,e); + throw new IOException("Failed to validate the SSL certificate of "+url,e); } } } @@ -1314,7 +1314,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas } catch (RestartRequiredException e) { throw new SuccessButRequiresRestart(e.message); } catch (Exception e) { - throw new IOException2("Failed to dynamically deploy this plugin",e); + throw new IOException("Failed to dynamically deploy this plugin",e); } } else { throw new SuccessButRequiresRestart(Messages._UpdateCenter_DownloadButNotActivated()); @@ -1544,7 +1544,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas * * This has to wait until after all plugins load, to let custom UpdateCenterConfiguration take effect first. */ - @Initializer(after=PLUGINS_STARTED) + @Initializer(after=PLUGINS_STARTED, fatal=false) public static void init(Jenkins h) throws IOException { h.getUpdateCenter().load(); } diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index 470ecaccabf633eca89a8393ee27b563b4f27e96..7d4ddd2bfea22e3726535fbdcec0927b3dc431fc 100644 --- a/core/src/main/java/hudson/model/UpdateSite.java +++ b/core/src/main/java/hudson/model/UpdateSite.java @@ -27,32 +27,18 @@ package hudson.model; import hudson.PluginManager; import hudson.PluginWrapper; -import hudson.ProxyConfiguration; import hudson.lifecycle.Lifecycle; import hudson.model.UpdateCenter.UpdateCenterJob; import hudson.util.FormValidation; import hudson.util.FormValidation.Kind; import hudson.util.HttpResponses; -import hudson.util.IOUtils; import hudson.util.TextFile; +import static hudson.util.TimeUnit2.*; import hudson.util.VersionNumber; -import jenkins.model.Jenkins; -import jenkins.util.JSONSignatureValidator; -import net.sf.json.JSONException; -import net.sf.json.JSONObject; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.HttpResponse; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.export.Exported; -import org.kohsuke.stapler.export.ExportedBean; -import org.kohsuke.stapler.interceptor.RequirePOST; - import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.net.URI; import java.net.URL; -import java.net.URLConnection; import java.net.URLEncoder; import java.security.GeneralSecurityException; import java.util.ArrayList; @@ -66,9 +52,23 @@ import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; - -import static hudson.util.TimeUnit2.*; - +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import jenkins.model.Jenkins; +import jenkins.model.DownloadSettings; +import jenkins.util.JSONSignatureValidator; +import net.sf.json.JSONException; +import net.sf.json.JSONObject; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.HttpResponse; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.export.Exported; +import org.kohsuke.stapler.export.ExportedBean; +import org.kohsuke.stapler.interceptor.RequirePOST; /** * Source of the update center information, like "http://jenkins-ci.org/update-center.json" @@ -153,42 +153,30 @@ public class UpdateSite { * @return null if no updates are necessary, or the future result * @since 1.502 */ - public Future updateDirectly(final boolean signatureCheck) { + public @CheckForNull Future updateDirectly(final boolean signatureCheck) { if (! getDataFile().exists() || isDue()) { return Jenkins.getInstance().getUpdateCenter().updateService.submit(new Callable() { - - public FormValidation call() throws Exception { - URL src = new URL(getUrl() + "?id=" + URLEncoder.encode(getId(),"UTF-8") - + "&version="+URLEncoder.encode(Jenkins.VERSION, "UTF-8")); - URLConnection conn = ProxyConfiguration.open(src); - InputStream is = conn.getInputStream(); - try { - String uncleanJson = IOUtils.toString(is,"UTF-8"); - int jsonStart = uncleanJson.indexOf("{\""); - if (jsonStart >= 0) { - uncleanJson = uncleanJson.substring(jsonStart); - int end = uncleanJson.lastIndexOf('}'); - if (end>0) - uncleanJson = uncleanJson.substring(0,end+1); - return updateData(uncleanJson, signatureCheck); - } else { - throw new IOException("Could not find json in content of " + - "update center from url: "+src.toExternalForm()); - } - } finally { - if (is != null) - is.close(); - } + @Override public FormValidation call() throws Exception { + return updateDirectlyNow(signatureCheck); } }); - } + } else { return null; + } + } + + @Restricted(NoExternalUse.class) + public @Nonnull FormValidation updateDirectlyNow(boolean signatureCheck) throws IOException { + return updateData(DownloadService.loadJSON(new URL(getUrl() + "?id=" + URLEncoder.encode(getId(), "UTF-8") + "&version=" + URLEncoder.encode(Jenkins.VERSION, "UTF-8"))), signatureCheck); } /** * This is the endpoint that receives the update center data file from the browser. */ public FormValidation doPostBack(StaplerRequest req) throws IOException, GeneralSecurityException { + if (!DownloadSettings.get().isUseBrowser()) { + throw new IOException("not allowed"); + } return updateData(IOUtils.toString(req.getInputStream(),"UTF-8"), true); } @@ -637,8 +625,12 @@ public class UpdateSite { } public String getDisplayName() { - if(title!=null) return title; - return name; + String displayName; + if(title!=null) + displayName = title; + else + displayName = name; + return StringUtils.removeStart(displayName, "Jenkins "); } /** diff --git a/core/src/main/java/hudson/model/UsageStatistics.java b/core/src/main/java/hudson/model/UsageStatistics.java index 2d153486704f6d04ff6cc09e3776044220e2c002..805403216b0aad6aaea35fe30e37972772a20a47 100644 --- a/core/src/main/java/hudson/model/UsageStatistics.java +++ b/core/src/main/java/hudson/model/UsageStatistics.java @@ -28,6 +28,7 @@ import hudson.PluginWrapper; import hudson.Util; import hudson.Extension; import hudson.node_monitors.ArchitectureMonitor.DescriptorImpl; +import hudson.util.IOUtils; import hudson.util.Secret; import static hudson.util.TimeUnit2.DAYS; @@ -135,6 +136,7 @@ public class UsageStatistics extends PageDecorator { if(c.getNode()==j) { n.put("master",true); n.put("jvm-vendor", System.getProperty("java.vm.vendor")); + n.put("jvm-name", System.getProperty("java.vm.name")); n.put("jvm-version", System.getProperty("java.version")); } n.put("executors",c.getNumExecutors()); @@ -155,7 +157,7 @@ public class UsageStatistics extends PageDecorator { o.put("plugins",plugins); JSONObject jobs = new JSONObject(); - List items = j.getItems(); + List items = j.getAllItems(TopLevelItem.class); for (TopLevelItemDescriptor d : Items.all()) { int cnt=0; for (TopLevelItem item : items) { @@ -171,8 +173,11 @@ public class UsageStatistics extends PageDecorator { // json -> UTF-8 encode -> gzip -> encrypt -> base64 -> string OutputStreamWriter w = new OutputStreamWriter(new GZIPOutputStream(new CombinedCipherOutputStream(baos,getKey(),"AES")), "UTF-8"); - o.write(w); - w.close(); + try { + o.write(w); + } finally { + IOUtils.closeQuietly(w); + } return new String(Base64.encode(baos.toByteArray())); } catch (GeneralSecurityException e) { diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java index a2f95c40e3a2a2abd92423bf827973e0993160d1..2d3d4b1444d980f0ae944b9933c5b6a70519bf5d 100644 --- a/core/src/main/java/hudson/model/User.java +++ b/core/src/main/java/hudson/model/User.java @@ -24,6 +24,7 @@ */ package hudson.model; +import com.google.common.base.Predicate; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import hudson.*; import hudson.model.Descriptor.FormException; @@ -32,11 +33,15 @@ import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.Permission; import hudson.security.SecurityRealm; +import hudson.security.UserMayOrMayNotExistException; import hudson.util.FormApply; import hudson.util.RunList; import hudson.util.XStream2; +import jenkins.model.IdStrategy; import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithContextMenu; +import jenkins.security.ImpersonatingUserDetailsService; +import jenkins.security.LastGrantedAuthoritiesProperty; import net.sf.json.JSONObject; import org.acegisecurity.Authentication; @@ -53,6 +58,7 @@ import org.kohsuke.stapler.export.ExportedBean; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.kohsuke.stapler.interceptor.RequirePOST; +import javax.annotation.concurrent.GuardedBy; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import java.io.File; @@ -65,11 +71,12 @@ import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; @@ -121,8 +128,21 @@ public class User extends AbstractModelObject implements AccessControlled, Descr load(); } + /** + * Returns the {@link jenkins.model.IdStrategy} for use with {@link User} instances. See + * {@link hudson.security.SecurityRealm#getUserIdStrategy()} + * + * @return the {@link jenkins.model.IdStrategy} for use with {@link User} instances. + * @since 1.566 + */ + @Nonnull + public static IdStrategy idStrategy() { + SecurityRealm realm = Jenkins.getInstance().getSecurityRealm(); + return realm == null ? IdStrategy.CASE_INSENSITIVE : realm.getUserIdStrategy(); + } + public int compareTo(User that) { - return this.id.compareTo(that.id); + return idStrategy().compare(this.id, that.id); } /** @@ -165,11 +185,11 @@ public class User extends AbstractModelObject implements AccessControlled, Descr } public String getUrl() { - return "user/"+Util.rawEncode(id); + return "user/"+Util.rawEncode(idStrategy().keyFor(id)); } public String getSearchUrl() { - return "/user/"+Util.rawEncode(id); + return "/user/"+Util.rawEncode(idStrategy().keyFor(id)); } /** @@ -247,19 +267,31 @@ public class User extends AbstractModelObject implements AccessControlled, Descr /** * Creates an {@link Authentication} object that represents this user. - * + * + * This method checks with {@link SecurityRealm} if the user is a valid user that can login to the security realm. + * If {@link SecurityRealm} is a kind that does not support querying information about other users, this will + * use {@link LastGrantedAuthoritiesProperty} to pick up the granted authorities as of the last time the user has + * logged in. + * + * @throws UsernameNotFoundException + * If this user is not a valid user in the backend {@link SecurityRealm}. * @since 1.419 */ - public Authentication impersonate() { + public Authentication impersonate() throws UsernameNotFoundException { try { - UserDetails u = Jenkins.getInstance().getSecurityRealm().loadUserByUsername(id); + UserDetails u = new ImpersonatingUserDetailsService( + Jenkins.getInstance().getSecurityRealm().getSecurityComponents().userDetails).loadUserByUsername(id); return new UsernamePasswordAuthenticationToken(u.getUsername(), "", u.getAuthorities()); + } catch (UserMayOrMayNotExistException e) { + // backend can't load information about other users. so use the stored information if available } catch (UsernameNotFoundException e) { - // ignore + // if the user no longer exists in the backend, we need to refuse impersonating this user + throw e; } catch (DataAccessException e) { - // ignore + // seems like it's in the same boat as UserMayOrMayNotExistException } - // TODO: use the stored GrantedAuthorities + + // seems like a legitimate user we have no idea about. proceed with minimum access return new UsernamePasswordAuthenticationToken(id, "", new GrantedAuthority[]{SecurityRealm.AUTHENTICATED_AUTHORITY}); } @@ -338,17 +370,36 @@ public class User extends AbstractModelObject implements AccessControlled, Descr * retrieve a user by its ID, and create a new one if requested */ private static User getOrCreate(String id, String fullName, boolean create) { - String idkey = id.toLowerCase(Locale.ENGLISH); + String idkey = idStrategy().keyFor(id); - User u = byName.get(idkey); + byNameLock.readLock().lock(); + User u; + try { + u = byName.get(idkey); + } finally { + byNameLock.readLock().unlock(); + } if (u==null && (create || getConfigFileFor(id).exists())) { User tmp = new User(id, fullName); - User prev = byName.putIfAbsent(idkey, u = tmp); + User prev; + byNameLock.readLock().lock(); + try { + prev = byName.putIfAbsent(idkey, u = tmp); + } finally { + byNameLock.readLock().unlock(); + } if (prev != null) { u = prev; // if some has already put a value in the map, use it if (LOGGER.isLoggable(Level.FINE) && !fullName.equals(prev.getFullName())) { LOGGER.log(Level.FINE, "mismatch on fullName (‘" + fullName + "’ vs. ‘" + prev.getFullName() + "’) for ‘" + id + "’", new Throwable()); } + } else if (!id.equals(fullName) && !getConfigFileFor(id).exists()) { + // JENKINS-16332: since the fullName may not be recoverable from the id, and various code may store the id only, we must save the fullName + try { + u.save(); + } catch (IOException x) { + LOGGER.log(Level.WARNING, null, x); + } } } return u; @@ -370,7 +421,10 @@ public class User extends AbstractModelObject implements AccessControlled, Descr Authentication a = Jenkins.getAuthentication(); if(a instanceof AnonymousAuthenticationToken) return null; - return get(a.getName()); + + // Since we already know this is a name, we can just call getOrCreate with the name directly. + String id = a.getName(); + return getOrCreate(id, id, true); } private static volatile long lastScanned; @@ -398,10 +452,18 @@ public class User extends AbstractModelObject implements AccessControlled, Descr lastScanned = System.currentTimeMillis(); } - ArrayList r = new ArrayList(byName.values()); + byNameLock.readLock().lock(); + ArrayList r; + try { + r = new ArrayList(byName.values()); + } finally { + byNameLock.readLock().unlock(); + } Collections.sort(r,new Comparator() { + IdStrategy strategy = idStrategy(); + public int compare(User o1, User o2) { - return o1.getId().compareToIgnoreCase(o2.getId()); + return strategy.compare(o1.getId(), o2.getId()); } }); return r; @@ -411,15 +473,47 @@ public class User extends AbstractModelObject implements AccessControlled, Descr * Reloads the configuration from disk. */ public static void reload() { - for( User u : byName.values() ) - u.load(); + byNameLock.readLock().lock(); + try { + for (User u : byName.values()) { + u.load(); + } + } finally { + byNameLock.readLock().unlock(); + } } /** * Stop gap hack. Don't use it. To be removed in the trunk. */ public static void clear() { - byName.clear(); + byNameLock.writeLock().lock(); + try { + byName.clear(); + } finally { + byNameLock.writeLock().unlock(); + } + } + + /** + * Called when changing the {@link IdStrategy}. + * @since 1.566 + */ + public static void rekey() { + final IdStrategy strategy = idStrategy(); + byNameLock.writeLock().lock(); + try { + for (Map.Entry e : byName.entrySet()) { + String idkey = strategy.keyFor(e.getValue().id); + if (!idkey.equals(e.getKey())) { + // need to remap + byName.remove(e.getKey()); + byName.putIfAbsent(idkey, e.getValue()); + } + } + } finally { + byNameLock.writeLock().unlock(); + } } /** @@ -429,30 +523,33 @@ public class User extends AbstractModelObject implements AccessControlled, Descr return getFullName(); } + /** true if {@link AbstractBuild#hasParticipant} or {@link hudson.model.Cause.UserIdCause} */ + private boolean relatedTo(AbstractBuild b) { + if (b.hasParticipant(this)) { + return true; + } + for (Cause cause : b.getCauses()) { + if (cause instanceof Cause.UserIdCause) { + String userId = ((Cause.UserIdCause) cause).getUserId(); + if (userId != null && idStrategy().equals(userId, getId())) { + return true; + } + } + } + return false; + } + /** * Gets the list of {@link Build}s that include changes by this user, * by the timestamp order. - * - * TODO: do we need some index for this? */ @WithBridgeMethods(List.class) public RunList getBuilds() { - List r = new ArrayList(); - for (AbstractProject p : Jenkins.getInstance().getAllItems(AbstractProject.class)) - for (AbstractBuild b : p.getBuilds().newBuilds()){ - if(b.hasParticipant(this)) - r.add(b); - else { - //append builds that were run by this user - Cause.UserIdCause cause = b.getCause(Cause.UserIdCause.class); - if (cause != null) { - String userId = cause.getUserId(); - if (userId != null && this.getId() != null && userId.equals(this.getId())) - r.add(b); - } - } + return new RunList>(Jenkins.getInstance().getAllItems(Job.class)).filter(new Predicate>() { + @Override public boolean apply(Run r) { + return r instanceof AbstractBuild && relatedTo((AbstractBuild) r); } - return RunList.fromRuns(r); + }); } /** @@ -479,7 +576,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr } private static final File getConfigFileFor(String id) { - return new File(getRootDir(),id +"/config.xml"); + return new File(getRootDir(), idStrategy().filenameOf(id) +"/config.xml"); } /** @@ -505,8 +602,14 @@ public class User extends AbstractModelObject implements AccessControlled, Descr * if we fail to delete. */ public synchronized void delete() throws IOException { - byName.remove(id.toLowerCase(Locale.ENGLISH)); - Util.deleteRecursive(new File(getRootDir(), id)); + final IdStrategy strategy = idStrategy(); + byNameLock.readLock().lock(); + try { + byName.remove(strategy.keyFor(id)); + } finally { + byNameLock.readLock().unlock(); + } + Util.deleteRecursive(new File(getRootDir(), strategy.filenameOf(id))); } /** @@ -559,7 +662,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr @RequirePOST public void doDoDelete(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { checkPermission(Jenkins.ADMINISTER); - if (id.equals(Jenkins.getAuthentication().getName())) { + if (idStrategy().equals(id, Jenkins.getAuthentication().getName())) { rsp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Cannot delete self"); return; } @@ -570,21 +673,18 @@ public class User extends AbstractModelObject implements AccessControlled, Descr } public void doRssAll(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { - rss(req, rsp, " all builds", RunList.fromRuns(getBuilds()), Run.FEED_ADAPTER); + rss(req, rsp, " all builds", getBuilds(), Run.FEED_ADAPTER); } public void doRssFailed(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { - rss(req, rsp, " regression builds", RunList.fromRuns(getBuilds()).regressionOnly(), Run.FEED_ADAPTER); + rss(req, rsp, " regression builds", getBuilds().regressionOnly(), Run.FEED_ADAPTER); } public void doRssLatest(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { final List lastBuilds = new ArrayList(); - for (final TopLevelItem item : Jenkins.getInstance().getItems()) { - if (!(item instanceof Job)) continue; - for (Run r = ((Job) item).getLastBuild(); r != null; r = r.getPreviousBuild()) { - if (!(r instanceof AbstractBuild)) continue; - final AbstractBuild b = (AbstractBuild) r; - if (b.hasParticipant(this)) { + for (AbstractProject p : Jenkins.getInstance().getAllItems(AbstractProject.class)) { + for (AbstractBuild b = p.getLastBuild(); b != null; b = b.getPreviousBuild()) { + if (relatedTo(b)) { lastBuilds.add(b); break; } @@ -602,10 +702,19 @@ public class User extends AbstractModelObject implements AccessControlled, Descr * Keyed by {@link User#id}. This map is used to ensure * singleton-per-id semantics of {@link User} objects. * - * The key needs to be lower cased for case insensitivity. + * The key needs to be generated by {@link IdStrategy#keyFor(String)}. */ + @GuardedBy("byNameLock") private static final ConcurrentMap byName = new ConcurrentHashMap(); + /** + * This lock is used to guard access to the {@link #byName} map. Use + * {@link java.util.concurrent.locks.ReadWriteLock#readLock()} for normal access and + * {@link java.util.concurrent.locks.ReadWriteLock#writeLock()} for {@link #rekey()} or any other operation + * that requires operating on the map as a whole. + */ + private static final ReadWriteLock byNameLock = new ReentrantReadWriteLock(); + /** * Used to load/save user configuration. */ @@ -622,7 +731,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr // always allow a non-anonymous user full control of himself. return new ACL() { public boolean hasPermission(Authentication a, Permission permission) { - return (a.getName().equals(id) && !(a instanceof AnonymousAuthenticationToken)) + return (idStrategy().equals(a.getName(), id) && !(a instanceof AnonymousAuthenticationToken)) || base.hasPermission(a, permission); } }; @@ -640,8 +749,9 @@ public class User extends AbstractModelObject implements AccessControlled, Descr * With ADMINISTER permission, can delete users with persisted data but can't delete self. */ public boolean canDelete() { - return hasPermission(Jenkins.ADMINISTER) && !id.equals(Jenkins.getAuthentication().getName()) - && new File(getRootDir(), id).exists(); + final IdStrategy strategy = idStrategy(); + return hasPermission(Jenkins.ADMINISTER) && !strategy.equals(id, Jenkins.getAuthentication().getName()) + && new File(getRootDir(), strategy.filenameOf(id)).exists(); } /** @@ -661,10 +771,11 @@ public class User extends AbstractModelObject implements AccessControlled, Descr continue; } String n = a.getAuthority(); - if (n != null && !n.equals(id)) { + if (n != null && !idStrategy().equals(n, id)) { r.add(n); } } + Collections.sort(r, String.CASE_INSENSITIVE_ORDER); return r; } @@ -764,5 +875,6 @@ public class User extends AbstractModelObject implements AccessControlled, Descr return -1; // lower than default } } + } diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java index 2944ea9946400f98a42bcfa820cafaa4dd7f9b44..ae586e49991d565d9debb931fdeba7fa4d32b3e9 100644 --- a/core/src/main/java/hudson/model/View.java +++ b/core/src/main/java/hudson/model/View.java @@ -35,6 +35,7 @@ import hudson.Indenter; import hudson.Util; import hudson.model.Descriptor.FormException; import hudson.model.labels.LabelAtomPropertyDescriptor; +import hudson.model.listeners.ItemListener; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import hudson.search.CollectionSearchIndex; @@ -50,7 +51,6 @@ import hudson.util.AlternativeUiTextProvider.Message; import hudson.util.DescribableList; import hudson.util.DescriptorList; import hudson.util.FormApply; -import hudson.util.IOException2; import hudson.util.RunList; import hudson.util.XStream2; import hudson.views.ListViewColumn; @@ -61,6 +61,7 @@ import jenkins.util.ProgressiveRendering; import net.sf.json.JSON; import net.sf.json.JSONArray; import net.sf.json.JSONObject; +import org.apache.tools.ant.filters.StringInputStream; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.Stapler; @@ -83,6 +84,7 @@ import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; @@ -113,7 +115,7 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; * This is an extension point in Hudson, allowing different kind of * rendering to be added as plugins. * - *

Note for implementors

+ *

Note for implementers

*