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 7c9af026e31056b734cfdeeae3b59e92b0b15d79..0000000000000000000000000000000000000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 0612606b040e8bd0b787031c0d9fe3540979439f..0000000000000000000000000000000000000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/.idea/groovyc.xml b/.idea/groovyc.xml deleted file mode 100644 index 117b3f5aa7c7674f57ac7a762b95f81ec91904e0..0000000000000000000000000000000000000000 --- a/.idea/groovyc.xml +++ /dev/null @@ -1,10 +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..f021c54d33f7f4b31d48b1b13474a7ffa3d1caa9 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 1000 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/assembly-src.xml b/assembly-src.xml index 851623064c9d27d0d0cd6340f8bf1de4f5ea93d0..555672f6ae88629ed4acce3582fd5fd6ead56c53 100644 --- a/assembly-src.xml +++ b/assembly-src.xml @@ -36,14 +36,14 @@ THE SOFTWARE. **/* - *.build - *.changes - *.pkg - *.rpm - *.deb - *.war - *.zip - *.log + **/*.build + **/*.changes + **/*.pkg + **/*.rpm + **/*.deb + **/*.war + **/*.zip + **/*.log .repository .repository/**/* **/target diff --git a/changelog.html b/changelog.html index c95be5195fafc9cb19822d699133fa2b024d1a2a..8167384b77be9c844f115fdc571e867852aa34db 100644 --- a/changelog.html +++ b/changelog.html @@ -56,5761 +56,2259 @@ Upcoming changes -

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

+

What's new in 1.593 (2014/12/07)

+

What's new in 1.593 (2014/11/30)

+ +

What's new in 1.592 (2014/11/30)

+ +

What's new in 1.591 (2014/11/25)

+ +

What's new in 1.590 (2014/11/16)

+ -

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

+

What's new in 1.589 (2014/11/09)

+

What's new in 1.588 (2014/11/02)

+ +

What's new in 1.587 (2014/10/29)

+ -

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

+

What's new in 1.586 (2014/10/26)

-

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

+

What's new in 1.585 (2014/10/19)

-

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

- -

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

+

What's new in 1.584 (2014/10/12)

-

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

+

What's new in 1.583 (2014/10/01)

-

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

+

What's new in 1.582 (2014/09/28)

-

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

+

What's new in 1.581 (2014/09/21)

-

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

+

What's new in 1.580 (2014/09/14)

+

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

+ -

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

+

What's new in 1.578 (2014/08/31)

+

What's new in 1.577 (2014/08/24)

+ -

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

+

What's new in 1.576 (2014/08/18)

-

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

- -

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.523 (2013/07/14)

- -

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

- -

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

- -

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

- -

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

- -

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

- -

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

- -

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

- -

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

- -

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

- -

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.575 (2014/08/10)

-

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

+

What's new in 1.574 (2014/07/27)

-

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

+

What's new in 1.573 (2014/07/20)

-

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

+

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

-

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

+

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

-

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

+

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

-

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

- -

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

+

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

-

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

+

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

-

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

+

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

-

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

+

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

-

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

+

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

-

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

+

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

-

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

+

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

-

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

+

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

-

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

+

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

-

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.560 (2014/04/20)

-

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.559 (2014/04/13)

-

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

+

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

-

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.557 (2014/03/31)

-

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

+

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

-

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

+

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

+

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

+ +

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

+ -

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

- -

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

+

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

-

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

+

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

-

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

- -

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

+

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

+

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

+ -

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

+

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

-

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

+

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

-

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

+

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

-

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

+

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

-

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

- -

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

+

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

-

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

+

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

+

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

+ -

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

+

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

-

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

- -

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

+

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

-

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

+

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

-

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

+

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

+

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

+ -

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

+

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

+

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

+ +

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

+ +

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

+ +

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

+ -

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

+

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

-

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

+

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

+

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

+ -

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

+

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

-

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 b68d1ec4861eba69e4eeee6cb6d0a2d6fb117f24..4b40627511d6a6a72fb5fb4255e574b9c78845de 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -5,7 +5,7 @@ pom org.jenkins-ci.main - 1.540-SNAPSHOT + 1.595-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 f8928eeaae995d9c84e9d4c0690803e3d951454f..6a3a05c28773b3bd56e4f93341b9c29df589df0f 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); @@ -392,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; @@ -400,6 +392,8 @@ public class CLI { if (url==null) url = System.getenv("HUDSON_URL"); + + boolean tryLoadPKey = true; while(!args.isEmpty()) { String head = args.get(0); @@ -426,23 +420,20 @@ public class CLI { args = args.subList(1,args.size()); continue; } + if (head.equals("-noKeyAuth")) { + tryLoadPKey = false; + args = args.subList(1,args.size()); + continue; + } if(head.equals("-i") && args.size()>=2) { File f = new File(args.get(1)); if (!f.exists()) { 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; @@ -463,8 +454,8 @@ public class CLI { if(args.isEmpty()) args = Arrays.asList("help"); // default to help - if (candidateKeys.isEmpty()) - addDefaultPrivateKeyLocations(candidateKeys); + if (tryLoadPKey && !provider.hasKeys()) + provider.readFromDefaultLocations(); CLIConnectionFactory factory = new CLIConnectionFactory().url(url).httpsProxyTunnel(httpProxy); String userInfo = new URL(url).getUserInfo(); @@ -474,10 +465,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"); @@ -529,103 +520,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.properties b/cli/src/main/resources/hudson/cli/client/Messages.properties index 567122ab9d49a01f6e0f95debdf0c6dd7a6e466a..699b4c47381227a92f7316331cbdbc3477759213 100644 --- a/cli/src/main/resources/hudson/cli/client/Messages.properties +++ b/cli/src/main/resources/hudson/cli/client/Messages.properties @@ -5,6 +5,7 @@ CLI.Usage=Jenkins CLI\n\ -i KEY : SSH private key file used for authentication\n\ -p HOST:PORT : HTTP proxy host and port for HTTPS proxy tunneling. See http://jenkins-ci.org/https-proxy-tunnel\n\ -noCertificateCheck : bypass HTTPS certificate check entirely. Use with caution\n\ + -noKeyAuth : don't try to load the SSH authentication private key. Conflicts with -i\n\ \n\ The available commands depend on the server. Run the 'help' command to\n\ see the list. 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..61e1895c4dfb7d5dd96cf0dd4723671fb60c673a 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 @@ -20,22 +20,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # 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\ -# \ -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\ -# see the list. +CLI.VersionMismatch=A vers\u00e3o n\u00e3o coincide. Esta CLI n\u00e3o pode funcionar com este servidor Jenkins 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\ + -i KEY : arquivo contendo a chave SSH privada usada para autentica\u00e7\u00e3o\n\ + -p HOST:PORT : host e porta do proxy HTTP para tunelamento de proxy HTTPS. Veja http://jenkins-ci.org/https-proxy-tunnel\n\ + -noCertificateCheck : ignora completamente a valida\u00e7\u00e3o dos certificados HTTPS. Use com cautela\n\ + -noKeyAuth : n\u00e3o tenta carregar a chave privada para autentica\u00e7\u00e3o SSH. Conflita com -i\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 +CLI.NoSuchFileExists=O arquivo n\u00e3o existe: {0} 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/ConnectionTest.java b/cli/src/test/java/hudson/cli/ConnectionTest.java index e9cc086affda83a0987cd65735986ce54553cb40..167198a182368440cb5fa045f2959b891c489ec4 100644 --- a/cli/src/test/java/hudson/cli/ConnectionTest.java +++ b/cli/src/test/java/hudson/cli/ConnectionTest.java @@ -28,7 +28,7 @@ public class ConnectionTest extends Assert { } @Test - public void testEncyrpt() throws Throwable { + public void testEncrypt() throws Throwable { final SecretKey sessionKey = new SecretKeySpec(new byte[16],"AES"); Thread t1 = new Thread() { @@ -56,15 +56,17 @@ public class ConnectionTest extends Assert { }; t2.start(); - t1.join(3000); - t2.join(3000); + t1.join(9999); + t2.join(9999); + + if (e != null) { + throw e; + } if (t1.isAlive() || t2.isAlive()) { t1.interrupt(); t2.interrupt(); throw new Error("thread is still alive"); } - - if (e!=null) throw e; } } 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/move-l10n.groovy b/core/move-l10n.groovy new file mode 100644 index 0000000000000000000000000000000000000000..d774ec50d4817573f8d4ba37a33632745a1f63b9 --- /dev/null +++ b/core/move-l10n.groovy @@ -0,0 +1,33 @@ +// Usage: groovy move-l10n.groovy hudson/model/OldClass/old-view jenkins/model/NewClass/new-view 'Some\ Translatable\ Text' +// (The new view may be given as '-' to simply delete the key.) + +def oldview = args[0]; +def newview = args[1]; +def key = args[2]; + +def scriptDir = new File(getClass().protectionDomain.codeSource.location.path).parent; +def resDir = new File(scriptDir, 'src/main/resources'); + +def basename = new File(resDir, oldview).name; +for (p in new File(resDir, oldview).parentFile.listFiles()) { + def n = p.name; + if (n == "${basename}.properties" || n.startsWith("${basename}_") && n.endsWith(".properties")) { + def lines = p.readLines('ISO-8859-1'); + def matches = lines.findAll({it.startsWith("${key}=")}); + if (!matches.isEmpty()) { + lines.removeAll(matches); + p.withWriter('ISO-8859-1') {out -> + lines.each {line -> out.writeLine(line)} + } + if (newview == '-') { + println("deleting ${matches.size()} matches from ${n}"); + } else { + def nue = new File(resDir, newview + n.substring(basename.length())); + println("moving ${matches.size()} matches from ${n} to ${nue.name}"); + nue.withWriterAppend('ISO-8859-1') {out -> + matches.each {line -> out.writeLine(line)} + } + } + } + } +} diff --git a/core/pom.xml b/core/pom.xml index dbd734511cb04d17e1bb8841d731df175e75b100..87c29d78a71e960a5625838114f5953acf91a87f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -29,7 +29,7 @@ THE SOFTWARE. org.jenkins-ci.main pom - 1.540-SNAPSHOT + 1.595-SNAPSHOT ../pom.xml @@ -42,12 +42,17 @@ THE SOFTWARE. true - 1.222 + 1.233 2.5.6.SEC03 1.8.9 + + org.jenkins-ci.plugins.icon-shim + icon-set + 1.0.3 + ${project.groupId} remoting @@ -97,7 +102,7 @@ THE SOFTWARE. org.jruby.ext.posix jna-posix - 1.0.3 + 1.0.3-jenkins-1 com.github.jnr @@ -112,7 +117,7 @@ THE SOFTWARE. org.jenkins-ci trilead-ssh2 - build217-jenkins-3 + build217-jenkins-8 org.kohsuke.stapler @@ -151,7 +156,7 @@ THE SOFTWARE. org.kohsuke.stapler stapler-adjunct-zeroclipboard - 1.1.7-1 + 1.3.5-1 org.kohsuke.stapler @@ -161,7 +166,7 @@ THE SOFTWARE. org.kohsuke.stapler stapler-adjunct-codemirror - 1.2 + 1.3 org.kohsuke.stapler @@ -174,7 +179,7 @@ THE SOFTWARE. com.infradna.tool bridge-method-annotation - 1.9 + 1.13 @@ -186,7 +191,7 @@ THE SOFTWARE. commons-httpclient commons-httpclient - + args4j args4j 2.0.23 @@ -194,17 +199,17 @@ THE SOFTWARE. org.jenkins-ci annotation-indexer - 1.6 + 1.7 org.jenkins-ci bytecode-compatibility-transformer - 1.4 + 1.5 - org.jvnet.hudson + org.jenkins-ci task-reactor - 1.2 + 1.4 org.jvnet.localizer @@ -219,7 +224,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 +484,12 @@ THE SOFTWARE. org.jvnet.winp winp - 1.16 + 1.22 org.jenkins-ci memory-monitor - 1.7 + 1.9 org.codehaus.woodstox @@ -489,7 +504,7 @@ THE SOFTWARE. net.java.dev.jna jna - 3.3.0-jenkins-3 + 4.1.0 org.kohsuke @@ -499,7 +514,7 @@ THE SOFTWARE. org.kohsuke libpam4j - 1.6 + 1.8 org.jvnet.libzfs @@ -517,14 +532,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 @@ -559,9 +569,9 @@ THE SOFTWARE. - org.kohsuke - owasp-html-sanitizer - r88 + commons-fileupload + commons-fileupload + 1.3.1-jenkins-1 @@ -583,11 +593,11 @@ THE SOFTWARE. com.google.guava guava - + com.jcraft jzlib - 1.1.3 + 1.1.3-kohsuke-1 @@ -640,7 +650,6 @@ THE SOFTWARE. com.infradna.tool bridge-method-injector - 1.9 @@ -728,7 +737,7 @@ THE SOFTWARE. com.sun.winsw winsw - 1.13 + 1.16 bin exe ${project.build.outputDirectory}/windows-service @@ -787,7 +796,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 481fa468712fc9e6fbe919c87727145a76750118..75d1414acc7273d3b032a2dc048bb56f40df3263 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,15 @@ 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; + +import static org.apache.commons.io.FilenameUtils.getBaseName; public class ClassicPluginStrategy implements PluginStrategy { - private final ClassLoaderReflectionToolkit clt = new ClassLoaderReflectionToolkit(); /** * Filter for jar files. @@ -95,41 +96,74 @@ public class ClassicPluginStrategy implements PluginStrategy { this.pluginManager = pluginManager; } - public PluginWrapper createPluginWrapper(File archive) throws IOException { + @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.getName()); + } + + private static boolean isLinked(File archive) { + return archive.getName().endsWith(".hpl") || archive.getName().endsWith(".jpl"); + } + + private static Manifest loadLinkedManifest(File archive) throws IOException { + // resolve the .hpl file to the location of the manifest file + try { + // Locate the manifest + String firstLine; + FileInputStream manifestHeaderInput = new FileInputStream(archive); + try { + firstLine = IOUtils.readFirstLine(manifestHeaderInput, "UTF-8"); + } finally { + manifestHeaderInput.close(); + } + if (firstLine.startsWith("Manifest-Version:")) { + // this is the manifest already + } else { + // indirection + archive = resolve(archive, firstLine); + } + + // Read the manifest + FileInputStream manifestInput = new FileInputStream(archive); + try { + return new Manifest(manifestInput); + } finally { + manifestInput.close(); + } + } catch (IOException e) { + throw new IOException("Failed to load " + archive, e); + } + } + + @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 = archive.getName().endsWith(".hpl") || archive.getName().endsWith(".jpl"); + boolean isLinked = isLinked(archive); if (isLinked) { - // 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:")) { - // this is the manifest already - } else { - // indirection - archive = resolve(archive, firstLine); - } - // then parse manifest - FileInputStream in = new FileInputStream(archive); - try { - manifest = new Manifest(in); - } catch (IOException e) { - throw new IOException2("Failed to load " + archive, e); - } finally { - in.close(); - } + manifest = loadLinkedManifest(archive); } else { if (archive.isDirectory()) {// already expanded expandDir = archive; } else { - expandDir = new File(archive.getParentFile(), PluginWrapper.getBaseName(archive)); + expandDir = new File(archive.getParentFile(), getBaseName(archive.getName())); explode(archive, expandDir); } - File manifestFile = new File(expandDir, "META-INF/MANIFEST.MF"); + File manifestFile = new File(expandDir, PluginWrapper.MANIFEST_FILENAME); if (!manifestFile.exists()) { throw new IOException( "Plugin installation failed. No manifest at " @@ -273,7 +307,11 @@ public class ClassicPluginStrategy implements PluginStrategy { new DetachedPlugin("ldap","1.467.*","1.0"), new DetachedPlugin("pam-auth","1.467.*","1.0"), new DetachedPlugin("mailer","1.493.*","1.2"), - new DetachedPlugin("matrix-auth","1.535.*","1.0.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"), + new DetachedPlugin("junit","1.577.*","1.0") ); /** @@ -314,7 +352,7 @@ public class ClassicPluginStrategy implements PluginStrategy { List> r = Lists.newArrayList(); for (ExtensionFinder finder : finders) { try { - r.addAll(finder._find(type, hudson)); + r.addAll(finder.find(type, hudson)); } catch (AbstractMethodError e) { // backward compatibility for (T t : finder.findExtensions(type, hudson)) @@ -350,13 +388,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); } } @@ -367,7 +405,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); @@ -378,6 +416,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()) @@ -427,7 +495,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 { @@ -524,6 +592,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() { @@ -568,10 +641,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 } } @@ -595,15 +668,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); @@ -621,14 +690,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); @@ -645,8 +710,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; diff --git a/core/src/main/java/hudson/EnvVars.java b/core/src/main/java/hudson/EnvVars.java index b8600a5ddfc6b1f15c2925f0747f7739c656de52..b2d61ead561da8fdabe82bb65a6c8aee46ff630e 100644 --- a/core/src/main/java/hudson/EnvVars.java +++ b/core/src/main/java/hudson/EnvVars.java @@ -23,12 +23,12 @@ */ 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 jenkins.security.MasterToSlaveCallable; import java.io.File; import java.io.IOException; @@ -356,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. @@ -396,7 +404,7 @@ public class EnvVars extends TreeMap { return channel.call(new GetEnvVars()); } - private static final class GetEnvVars implements Callable { + private static final class GetEnvVars extends MasterToSlaveCallable { public EnvVars call() { return new EnvVars(EnvVars.masterEnvVars); } diff --git a/core/src/main/java/hudson/ExtensionFinder.java b/core/src/main/java/hudson/ExtensionFinder.java index c63c9488987e71630d242e657dd4a154ea11bf14..d4dc1ef31bc7ebe0b20ea152a53275044e94f25f 100644 --- a/core/src/main/java/hudson/ExtensionFinder.java +++ b/core/src/main/java/hudson/ExtensionFinder.java @@ -143,10 +143,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { */ public abstract Collection> find(Class type, Hudson jenkins); - /** - * A pointless function to work around what appears to be a HotSpot problem. See JENKINS-5756 and bug 6933067 - * on BugParade for more details. - */ + @Deprecated public Collection> _find(Class type, Hudson hudson) { return find(type, hudson); } @@ -284,6 +281,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { LOGGER.log(Level.SEVERE, "Failed to create Guice container from all the plugins",e); // failing to load all bindings are disastrous, so recover by creating minimum that works // by just including the core + // TODO this recovery is pretty much useless; startup crashes anyway container = Guice.createInjector(new SezpozModule(loadSezpozIndices(Jenkins.class.getClassLoader()))); } @@ -358,7 +356,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { // which results in a LinkageError LOGGER.log(isOptional(item.annotation()) ? Level.FINE : Level.WARNING, "Failed to load "+item.className(), e); - } catch (InstantiationException e) { + } catch (Exception e) { LOGGER.log(isOptional(item.annotation()) ? Level.FINE : Level.WARNING, "Failed to load "+item.className(), e); } @@ -481,8 +479,10 @@ public abstract class ExtensionFinder implements ExtensionPoint { Method m = ClassLoader.class.getDeclaredMethod("resolveClass", Class.class); m.setAccessible(true); m.invoke(ecl, c); + c.getConstructors(); c.getMethods(); - c.getFields(); + c.getFields(); + LOGGER.log(Level.FINER, "{0} looks OK", c); while (c != Object.class) { c.getGenericSuperclass(); c = c.getSuperclass(); @@ -537,7 +537,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { // which results in a LinkageError LOGGER.log(optional ? Level.FINE : Level.WARNING, "Failed to load "+item.className(), e); - } catch (InstantiationException e) { + } catch (Exception e) { LOGGER.log(optional ? Level.FINE : Level.WARNING, "Failed to load "+item.className(), e); } @@ -647,7 +647,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { // sometimes the instantiation fails in an indirect classloading failure, // which results in a LinkageError LOGGER.log(logLevel(item), "Failed to load "+item.className(), e); - } catch (InstantiationException e) { + } catch (Exception e) { LOGGER.log(logLevel(item), "Failed to load "+item.className(), e); } } @@ -678,9 +678,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { // according to http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6459208 // this appears to be the only way to force a class initialization Class.forName(extType.getName(),true,extType.getClassLoader()); - } catch (InstantiationException e) { - LOGGER.log(logLevel(item), "Failed to scout "+item.className(), e); - } catch (ClassNotFoundException e) { + } catch (Exception e) { LOGGER.log(logLevel(item), "Failed to scout "+item.className(), e); } catch (LinkageError e) { LOGGER.log(logLevel(item), "Failed to scout "+item.className(), e); diff --git a/core/src/main/java/hudson/ExtensionList.java b/core/src/main/java/hudson/ExtensionList.java index 0d1d8f2d7f386df3c7e97ab6c0b69bad5ce53abc..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(); + } } /** @@ -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 b2564edc70afb7de07ef5e418dd62ce1ee16e256..74760a428837196561c45e208655dbba31d6448b 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -25,42 +25,57 @@ */ package hudson; +import com.jcraft.jzlib.GZIPInputStream; +import com.jcraft.jzlib.GZIPOutputStream; import hudson.Launcher.LocalLauncher; import hudson.Launcher.RemoteLauncher; -import hudson.os.PosixAPI; -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.RemoteInputStream.Flag; 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.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.FilePathFilter; +import jenkins.MasterToSlaveFileCallable; +import jenkins.SlaveToMasterFileCallable; +import jenkins.SoloFilePathFilter; +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 +88,40 @@ import java.io.InterruptedIOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.RandomAccessFile; 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.*; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import jenkins.security.MasterToSlaveCallable; +import org.jenkinsci.remoting.RoleChecker; +import org.jenkinsci.remoting.RoleSensitive; /** * {@link File} like object with remoting support. @@ -191,6 +207,18 @@ public final class FilePath implements Serializable { // since the platform of the slave might be different, can't use java.io.File private final String remote; + /** + * If this {@link FilePath} is deserialized to handle file access request from a remote computer, + * this field is set to the filter that performs access control. + * + *

+ * If null, no access control is needed. + * + * @see #filterNonNull() + */ + private transient @Nullable + SoloFilePathFilter filter; + /** * Creates a {@link FilePath} that represents a path on the given node. * @@ -199,7 +227,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); } @@ -417,11 +445,11 @@ public final class FilePath implements Serializable { */ public int archive(final ArchiverFactory factory, OutputStream os, final DirScanner scanner) throws IOException, InterruptedException { final OutputStream out = (channel!=null)?new RemoteOutputStream(os):os; - return act(new FileCallable() { + return act(new SecureFileCallable() { public Integer invoke(File f, VirtualChannel channel) throws IOException { Archiver a = factory.create(out); try { - scanner.scan(f,a); + scanner.scan(f,reading(a)); } finally { a.close(); } @@ -449,17 +477,29 @@ public final class FilePath implements Serializable { * @see #unzipFrom(InputStream) */ public void unzip(final FilePath target) throws IOException, InterruptedException { - target.act(new FileCallable() { + // TODO: post release, re-unite two branches by introducing FileStreamCallable that resolves InputStream + if (this.channel!=target.channel) {// local -> remote or remote->local + final RemoteInputStream in = new RemoteInputStream(read(), Flag.GREEDY); + target.act(new SecureFileCallable() { + public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { + unzip(dir, in); + return null; + } - public Void invoke(File dir, VirtualChannel channel) throws IOException { - if (FilePath.this.isRemote()) - unzip(dir, FilePath.this.read()); // use streams - else - unzip(dir, new File(FilePath.this.getRemote())); // shortcut to local file - return null; - } - private static final long serialVersionUID = 1L; - }); + private static final long serialVersionUID = 1L; + }); + } else {// local -> local or remote->remote + target.act(new SecureFileCallable() { + + public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { + assert !FilePath.this.isRemote(); // this.channel==target.channel above + unzip(dir, reading(new File(FilePath.this.getRemote()))); // shortcut to local file + return null; + } + + private static final long serialVersionUID = 1L; + }); + } } /** @@ -473,13 +513,26 @@ public final class FilePath implements Serializable { * @see #untarFrom(InputStream, TarCompression) */ public void untar(final FilePath target, final TarCompression compression) throws IOException, InterruptedException { - target.act(new FileCallable() { - public Void invoke(File dir, VirtualChannel channel) throws IOException { - readFromTar(FilePath.this.getName(),dir,compression.extract(FilePath.this.read())); - return null; - } - private static final long serialVersionUID = 1L; - }); + // TODO: post release, re-unite two branches by introducing FileStreamCallable that resolves InputStream + if (this.channel!=target.channel) {// local -> remote or remote->local + final RemoteInputStream in = new RemoteInputStream(read(), Flag.GREEDY); + target.act(new SecureFileCallable() { + public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { + readFromTar(FilePath.this.getName(),dir,compression.extract(in)); + return null; + } + + private static final long serialVersionUID = 1L; + }); + } else {// local -> local or remote->remote + target.act(new SecureFileCallable() { + public Void invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { + readFromTar(FilePath.this.getName(),dir,compression.extract(FilePath.this.read())); + return null; + } + private static final long serialVersionUID = 1L; + }); + } } /** @@ -492,7 +545,7 @@ public final class FilePath implements Serializable { */ public void unzipFrom(InputStream _in) throws IOException, InterruptedException { final InputStream in = new RemoteInputStream(_in); - act(new FileCallable() { + act(new SecureFileCallable() { public Void invoke(File dir, VirtualChannel channel) throws IOException { unzip(dir, in); return null; @@ -501,7 +554,7 @@ public final class FilePath implements Serializable { }); } - private static void unzip(File dir, InputStream in) throws IOException { + private void unzip(File dir, InputStream in) throws IOException { File tmpFile = File.createTempFile("tmpzip", null); // uses java.io.tmpdir try { // TODO why does this not simply use ZipInputStream? @@ -513,7 +566,7 @@ public final class FilePath implements Serializable { } } - static private void unzip(File dir, File zipFile) throws IOException { + private void unzip(File dir, File zipFile) throws IOException { dir = dir.getAbsoluteFile(); // without absolutization, getParentFile below seems to fail ZipFile zip = new ZipFile(zipFile); @SuppressWarnings("unchecked") @@ -524,15 +577,15 @@ public final class FilePath implements Serializable { ZipEntry e = entries.nextElement(); File f = new File(dir, e.getName()); if (e.isDirectory()) { - f.mkdirs(); + mkdirs(f); } else { File p = f.getParentFile(); if (p != null) { - p.mkdirs(); + mkdirs(p); } InputStream input = zip.getInputStream(e); try { - IOUtils.copy(input, f); + IOUtils.copy(input, writing(f)); } finally { input.close(); } @@ -556,7 +609,7 @@ public final class FilePath implements Serializable { * Absolutizes this {@link FilePath} and returns the new one. */ public FilePath absolutize() throws IOException, InterruptedException { - return new FilePath(channel,act(new FileCallable() { + return new FilePath(channel,act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File f, VirtualChannel channel) throws IOException { return f.getAbsolutePath(); @@ -574,9 +627,10 @@ public final class FilePath implements Serializable { * @since 1.456 */ public void symlinkTo(final String target, final TaskListener listener) throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + symlinking(f); Util.createSymlink(f.getParentFile(),target,f.getName(),listener); return null; } @@ -591,10 +645,10 @@ public final class FilePath implements Serializable { * @since 1.456 */ public String readLink() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - return Util.resolveSymlink(f); + return Util.resolveSymlink(reading(f)); } }); } @@ -636,7 +690,7 @@ 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 { @@ -660,7 +714,7 @@ public final class FilePath implements Serializable { public void untarFrom(InputStream _in, final TarCompression compression) throws IOException, InterruptedException { try { final InputStream in = new RemoteInputStream(_in); - act(new FileCallable() { + act(new SecureFileCallable() { public Void invoke(File dir, VirtualChannel channel) throws IOException { readFromTar("input stream",dir, compression.extract(in)); return null; @@ -668,7 +722,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,17 +814,18 @@ 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); } } - private static final class Unpack implements FileCallable { + // this reads from arbitrary URL + private final class Unpack extends MasterToSlaveFileCallable { private final URL archive; Unpack(URL archive) { this.archive = archive; @@ -786,7 +841,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 +873,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(); } @@ -839,17 +894,17 @@ public final class FilePath implements Serializable { public void copyFrom(FileItem file) throws IOException, InterruptedException { if(channel==null) { try { - file.write(new File(remote)); + file.write(writing(new File(remote))); } 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(); @@ -864,9 +919,14 @@ public final class FilePath implements Serializable { * Code that gets executed on the machine where the {@link FilePath} is local. * Used to act on {@link FilePath}. * Warning: implementations must be serializable, so prefer a static nested class to an inner class. + * + *

+ * Subtypes would likely want to extend from either {@link MasterToSlaveCallable} + * or {@link SlaveToMasterFileCallable}. + * * @see FilePath#act(FileCallable) */ - public interface FileCallable extends Serializable { + public interface FileCallable extends Serializable, RoleSensitive { /** * Performs the computational task on the node where the data is located. * @@ -882,6 +942,15 @@ public final class FilePath implements Serializable { T invoke(File f, VirtualChannel channel) throws IOException, InterruptedException; } + /** + * {@link FileCallable}s that can be executed anywhere, including the master. + * + * The code is the same as {@link SlaveToMasterFileCallable}, but used as a marker to + * designate those impls that use {@link FilePathFilter}. + */ + /*package*/ static abstract class SecureFileCallable extends SlaveToMasterFileCallable { + } + /** * Executes some program on the machine that this {@link FilePath} exists, * so that one can perform local file operations. @@ -895,14 +964,9 @@ public final class FilePath implements Serializable { // run this on a remote system try { DelegatingCallable wrapper = new FileCallableWrapper(callable, cl); - Jenkins instance = Jenkins.getInstance(); - if (instance != null) { // this happens during unit tests - ExtensionList factories = instance.getExtensionList(FileCallableWrapperFactory.class); - for (FileCallableWrapperFactory factory : factories) { - wrapper = factory.wrap(wrapper); - } + for (FileCallableWrapperFactory factory : ExtensionList.lookup(FileCallableWrapperFactory.class)) { + wrapper = factory.wrap(wrapper); } - return channel.call(wrapper); } catch (TunneledInterruptedException e) { throw (InterruptedException)new InterruptedException(e.getMessage()).initCause(e); @@ -910,11 +974,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, 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); } } @@ -980,19 +1044,14 @@ public final class FilePath implements Serializable { public Future actAsync(final FileCallable callable) throws IOException, InterruptedException { try { DelegatingCallable wrapper = new FileCallableWrapper(callable); - Jenkins instance = Jenkins.getInstance(); - if (instance != null) { // this happens during unit tests - ExtensionList factories = instance.getExtensionList(FileCallableWrapperFactory.class); - for (FileCallableWrapperFactory factory : factories) { - wrapper = factory.wrap(wrapper); - } + for (FileCallableWrapperFactory factory : ExtensionList.lookup(FileCallableWrapperFactory.class)) { + wrapper = factory.wrap(wrapper); } - - 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); } } @@ -1027,6 +1086,12 @@ public final class FilePath implements Serializable { throw (IOException)new InterruptedIOException().initCause(e); } } + + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + task.checkRoles(checker); + } + private static final long serialVersionUID = 1L; }; } @@ -1036,7 +1101,7 @@ public final class FilePath implements Serializable { * on which this file is available. */ public URI toURI() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public URI invoke(File f, VirtualChannel channel) { return f.toURI(); @@ -1053,14 +1118,31 @@ 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. + * @since 1.571 + */ + 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. */ public void mkdirs() throws IOException, InterruptedException { - if(!act(new FileCallable() { + if(!act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - if(f.mkdirs() || f.exists()) + if(mkdirs(f) || f.exists()) return true; // OK // following Ant task to avoid possible race condition. @@ -1076,10 +1158,10 @@ public final class FilePath implements Serializable { * Deletes this directory, including all its contents recursively. */ public void deleteRecursive() throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteRecursive(f); + deleteRecursive(deleting(f)); return null; } }); @@ -1089,15 +1171,38 @@ public final class FilePath implements Serializable { * Deletes all the contents of this directory, but not the directory itself */ public void deleteContents() throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteContentsRecursive(f); + deleteContentsRecursive(f); return null; } }); } + private void deleteRecursive(File dir) throws IOException { + if(!isSymlink(dir)) + deleteContentsRecursive(dir); + try { + deleteFile(deleting(dir)); + } catch (IOException e) { + // if some of the child directories are big, it might take long enough to delete that + // it allows others to create new files, causing problemsl ike JENKINS-10113 + // so give it one more attempt before we give up. + if(!isSymlink(dir)) + deleteContentsRecursive(dir); + deleteFile(deleting(dir)); + } + } + + private void deleteContentsRecursive(File file) throws IOException { + File[] files = file.listFiles(); + if(files==null) + return; // the directory didn't exist in the first place + for (File child : files) + deleteRecursive(child); + } + /** * Gets the file name portion except the extension. * @@ -1149,7 +1254,7 @@ public final class FilePath implements Serializable { * @param relOrAbsolute a relative or absolute path * @return a file on the same channel */ - public FilePath child(String relOrAbsolute) { + public @Nonnull FilePath child(String relOrAbsolute) { return new FilePath(this,relOrAbsolute); } @@ -1183,15 +1288,15 @@ public final class FilePath implements Serializable { */ public FilePath createTempFile(final String prefix, final String suffix) throws IOException, InterruptedException { try { - return new FilePath(this,act(new FileCallable() { + return new FilePath(this,act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File dir, VirtualChannel channel) throws IOException { - File f = File.createTempFile(prefix, suffix, dir); + File f = writing(File.createTempFile(prefix, suffix, dir)); return f.getName(); } })); } 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); } } @@ -1239,22 +1344,22 @@ public final class FilePath implements Serializable { */ public FilePath createTextTempFile(final String prefix, final String suffix, final String contents, final boolean inThisDirectory) throws IOException, InterruptedException { try { - return new FilePath(channel,act(new FileCallable() { + return new FilePath(channel,act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File dir, VirtualChannel channel) throws IOException { if(!inThisDirectory) dir = new File(System.getProperty("java.io.tmpdir")); else - dir.mkdirs(); + mkdirs(dir); File f; try { - f = File.createTempFile(prefix, suffix, dir); + f = creating(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); + Writer w = new FileWriter(writing(f)); try { w.write(contents); } finally { @@ -1265,7 +1370,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); } } @@ -1285,7 +1390,7 @@ public final class FilePath implements Serializable { */ public FilePath createTempDir(final String prefix, final String suffix) throws IOException, InterruptedException { try { - return new FilePath(this,act(new FileCallable() { + return new FilePath(this,act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File dir, VirtualChannel channel) throws IOException { File f = File.createTempFile(prefix, suffix, dir); @@ -1295,7 +1400,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); } } @@ -1305,10 +1410,10 @@ public final class FilePath implements Serializable { * @return true, for a modicum of compatibility */ public boolean delete() throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - Util.deleteFile(f); + Util.deleteFile(deleting(f)); return null; } }); @@ -1319,10 +1424,10 @@ public final class FilePath implements Serializable { * Checks if the file exists. */ public boolean exists() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Boolean invoke(File f, VirtualChannel channel) throws IOException { - return f.exists(); + return stating(f).exists(); } }); } @@ -1335,10 +1440,10 @@ public final class FilePath implements Serializable { * @see #touch(long) */ public long lastModified() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Long invoke(File f, VirtualChannel channel) throws IOException { - return f.lastModified(); + return stating(f).lastModified(); } }); } @@ -1349,12 +1454,12 @@ public final class FilePath implements Serializable { * @since 1.299 */ public void touch(final long timestamp) throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = -5094638816500738429L; public Void invoke(File f, VirtualChannel channel) throws IOException { if(!f.exists()) - new FileOutputStream(f).close(); - if(!f.setLastModified(timestamp)) + new FileOutputStream(creating(f)).close(); + if(!stating(f).setLastModified(timestamp)) throw new IOException("Failed to set the timestamp of "+f+" to "+timestamp); return null; } @@ -1362,10 +1467,10 @@ public final class FilePath implements Serializable { } private void setLastModifiedIfPossible(final long timestamp) throws IOException, InterruptedException { - String message = act(new FileCallable() { + String message = act(new SecureFileCallable() { private static final long serialVersionUID = -828220335793641630L; public String invoke(File f, VirtualChannel channel) throws IOException { - if(!f.setLastModified(timestamp)) { + if(!writing(f).setLastModified(timestamp)) { if (Functions.isWindows()) { // On Windows this seems to fail often. See JENKINS-11073 // Therefore don't fail, but just log a warning @@ -1387,10 +1492,10 @@ public final class FilePath implements Serializable { * Checks if the file is a directory. */ public boolean isDirectory() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Boolean invoke(File f, VirtualChannel channel) throws IOException { - return f.isDirectory(); + return stating(f).isDirectory(); } }); } @@ -1401,10 +1506,49 @@ public final class FilePath implements Serializable { * @since 1.129 */ public long length() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Long invoke(File f, VirtualChannel channel) throws IOException { - return f.length(); + return stating(f).length(); + } + }); + } + + /** + * Returns the number of unallocated bytes in the partition of that file. + * @since 1.542 + */ + public long getFreeDiskSpace() throws IOException, InterruptedException { + return act(new SecureFileCallable() { + 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 SecureFileCallable() { + 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 SecureFileCallable() { + private static final long serialVersionUID = 1L; + @Override public Long invoke(File f, VirtualChannel channel) throws IOException { + return f.getUsableSpace(); } }); } @@ -1426,10 +1570,10 @@ public final class FilePath implements Serializable { */ public void chmod(final int mask) throws IOException, InterruptedException { if(!isUnix() || mask==-1) return; - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - _chmod(f, mask); + _chmod(writing(f), mask); return null; } @@ -1457,10 +1601,10 @@ public final class FilePath implements Serializable { */ public int mode() throws IOException, InterruptedException, PosixException { if(!isUnix()) return -1; - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Integer invoke(File f, VirtualChannel channel) throws IOException { - return IOUtils.mode(f); + return IOUtils.mode(stating(f)); } }); } @@ -1504,10 +1648,10 @@ public final class FilePath implements Serializable { if (filter != null && !(filter instanceof Serializable)) { throw new IllegalArgumentException("Non-serializable filter of " + filter.getClass()); } - return act(new FileCallable>() { + return act(new SecureFileCallable>() { private static final long serialVersionUID = 1L; public List invoke(File f, VirtualChannel channel) throws IOException { - File[] children = f.listFiles(filter); + File[] children = reading(f).listFiles(filter); if(children ==null) return null; ArrayList r = new ArrayList(children.length); @@ -1557,10 +1701,10 @@ public final class FilePath implements Serializable { * @since 1.465 */ public FilePath[] list(final String includes, final String excludes, final boolean defaultExcludes) throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public FilePath[] invoke(File f, VirtualChannel channel) throws IOException { - String[] files = glob(f, includes, excludes, defaultExcludes); + String[] files = glob(reading(f), includes, excludes, defaultExcludes); FilePath[] r = new FilePath[files.length]; for( int i=0; i() { + actAsync(new SecureFileCallable() { private static final long serialVersionUID = 1L; - public Void call() throws IOException { - FileInputStream fis=null; + + @Override + public Void invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + FileInputStream fis = null; try { - fis = new FileInputStream(new File(remote)); - Util.copyStream(fis,p.getOut()); - return null; + fis = new FileInputStream(reading(f)); + Util.copyStream(fis, p.getOut()); + } catch (Exception x) { + p.error(x); } finally { - IOUtils.closeQuietly(fis); - IOUtils.closeQuietly(p.getOut()); + org.apache.commons.io.IOUtils.closeQuietly(fis); + org.apache.commons.io.IOUtils.closeQuietly(p.getOut()); } + return null; } }); return p.getIn(); } + /** + * Reads this file from the specific offset. + * @since 1.586 + */ + public InputStream readFromOffset(final long offset) throws IOException, InterruptedException { + if(channel ==null) { + final RandomAccessFile raf = new RandomAccessFile(new File(remote), "r"); + try { + raf.seek(offset); + } catch (IOException e) { + try { + raf.close(); + } catch (IOException e1) { + // ignore + } + throw e; + } + return new InputStream() { + @Override + public int read() throws IOException { + return raf.read(); + } + + @Override + public void close() throws IOException { + raf.close(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return raf.read(b, off, len); + } + + @Override + public int read(byte[] b) throws IOException { + return raf.read(b); + } + }; + } + + final Pipe p = Pipe.createRemoteToLocal(); + actAsync(new SecureFileCallable() { + private static final long serialVersionUID = 1L; + + public Void invoke(File f, VirtualChannel channel) throws IOException { + final OutputStream out = new java.util.zip.GZIPOutputStream(p.getOut(), 8192); + RandomAccessFile raf = null; + try { + raf = new RandomAccessFile(reading(f), "r"); + raf.seek(offset); + byte[] buf = new byte[8192]; + int len; + while ((len = raf.read(buf)) >= 0) { + out.write(buf, 0, len); + } + return null; + } finally { + IOUtils.closeQuietly(out); + if (raf != null) { + try { + raf.close(); + } catch (IOException e) { + // ignore + } + } + } + } + }); + + return new java.util.zip.GZIPInputStream(p.getIn()); + } + /** * Reads this file into a string, by using the current system encoding. */ - public String readToString() throws IOException { + public String readToString() throws IOException, InterruptedException { InputStream in = read(); try { - return IOUtils.toString(in); + return org.apache.commons.io.IOUtils.toString(in); } finally { in.close(); } @@ -1643,16 +1863,16 @@ public final class FilePath implements Serializable { public OutputStream write() throws IOException, InterruptedException { if(channel==null) { File f = new File(remote).getAbsoluteFile(); - f.getParentFile().mkdirs(); - return new FileOutputStream(f); + mkdirs(f.getParentFile()); + return new FileOutputStream(writing(f)); } - return channel.call(new Callable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; - public OutputStream call() throws IOException { - File f = new File(remote).getAbsoluteFile(); - f.getParentFile().mkdirs(); - FileOutputStream fos = new FileOutputStream(f); + public OutputStream invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + f = f.getAbsoluteFile(); + mkdirs(f.getParentFile()); + FileOutputStream fos = new FileOutputStream(writing(f)); return new RemoteOutputStream(fos); } }); @@ -1666,11 +1886,11 @@ public final class FilePath implements Serializable { * @since 1.105 */ public void write(final String content, final String encoding) throws IOException, InterruptedException { - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - f.getParentFile().mkdirs(); - FileOutputStream fos = new FileOutputStream(f); + mkdirs(f.getParentFile()); + FileOutputStream fos = new FileOutputStream(writing(f)); Writer w = encoding != null ? new OutputStreamWriter(fos, encoding) : new OutputStreamWriter(fos); try { w.write(content); @@ -1687,10 +1907,10 @@ public final class FilePath implements Serializable { * @see Util#getDigestOf(File) */ public String digest() throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public String invoke(File f, VirtualChannel channel) throws IOException { - return Util.getDigestOf(f); + return Util.getDigestOf(reading(f)); } }); } @@ -1703,10 +1923,10 @@ public final class FilePath implements Serializable { if(this.channel != target.channel) { throw new IOException("renameTo target must be on the same host"); } - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { - f.renameTo(new File(target.remote)); + reading(f).renameTo(creating(new File(target.remote))); return null; } }); @@ -1721,17 +1941,23 @@ public final class FilePath implements Serializable { if(this.channel != target.channel) { throw new IOException("pullUpTo target must be on the same host"); } - act(new FileCallable() { + act(new SecureFileCallable() { 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 : reading(tmp).listFiles()) { File target = new File(t, child.getName()); - if(!child.renameTo(target)) + if(!stating(child).renameTo(creating(target))) throw new IOException("Failed to rename "+child+" to "+target); } - f.delete(); + deleting(tmp).delete(); return null; } }); @@ -1749,7 +1975,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); } } @@ -1770,17 +1996,17 @@ public final class FilePath implements Serializable { public void copyTo(OutputStream os) throws IOException, InterruptedException { final OutputStream out = new RemoteOutputStream(os); - act(new FileCallable() { + act(new SecureFileCallable() { private static final long serialVersionUID = 4088559042349254141L; public Void invoke(File f, VirtualChannel channel) throws IOException { FileInputStream fis = null; try { - fis = new FileInputStream(f); + fis = new FileInputStream(reading(f)); 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); } } }); @@ -1798,7 +2024,7 @@ public final class FilePath implements Serializable { private void syncIO() throws InterruptedException { try { if (channel!=null) - _syncIO(); + channel.syncLocalIO(); } catch (AbstractMethodError e) { // legacy slave.jar. Handle this gracefully try { @@ -1886,34 +2112,41 @@ public final class FilePath implements Serializable { public int copyRecursiveTo(final DirScanner scanner, final FilePath target, final String description) throws IOException, InterruptedException { if(this.channel==target.channel) { // local to local copy. - return act(new FileCallable() { + return act(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Integer invoke(File base, VirtualChannel channel) throws IOException { if(!base.exists()) return 0; assert target.channel==null; final File dest = new File(target.remote); final AtomicInteger count = new AtomicInteger(); - scanner.scan(base, new FileVisitor() { - @Override public void visit(File f, String relativePath) throws IOException { + scanner.scan(base, reading(new FileVisitor() { + @Override + public void visit(File f, String relativePath) throws IOException { if (f.isFile()) { File target = new File(dest, relativePath); - target.getParentFile().mkdirs(); - Util.copyFile(f, target); + mkdirsE(target.getParentFile()); + Util.copyFile(f, writing(target)); count.incrementAndGet(); } } - @Override public boolean understandsSymlink() { + + @Override + public boolean understandsSymlink() { return true; } - @Override public void visitSymlink(File link, String target, String relativePath) throws IOException { + + @Override + public void visitSymlink(File link, String target, String relativePath) throws IOException { try { + mkdirsE(new File(dest, relativePath).getParentFile()); + writing(new File(dest, target)); Util.createSymlink(dest, target, relativePath, TaskListener.NULL); } catch (InterruptedException x) { throw (IOException) new IOException(x.toString()).initCause(x); } count.incrementAndGet(); } - }); + })); return count.get(); } }); @@ -1922,7 +2155,7 @@ public final class FilePath implements Serializable { // local -> remote copy final Pipe pipe = Pipe.createLocalToRemote(); - Future future = target.actAsync(new FileCallable() { + Future future = target.actAsync(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { try { @@ -1933,7 +2166,7 @@ public final class FilePath implements Serializable { } } }); - Future future2 = actAsync(new FileCallable() { + Future future2 = actAsync(new SecureFileCallable() { private static final long serialVersionUID = 1L; @Override public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { return writeToTar(new File(remote), scanner, TarCompression.GZIP.compress(pipe.getOut())); @@ -1944,13 +2177,13 @@ 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 final Pipe pipe = Pipe.createRemoteToLocal(); - Future future = actAsync(new FileCallable() { + Future future = actAsync(new SecureFileCallable() { private static final long serialVersionUID = 1L; public Integer invoke(File f, VirtualChannel channel) throws IOException { try { @@ -1968,7 +2201,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; @@ -1977,7 +2210,7 @@ public final class FilePath implements Serializable { try { return future.get(); } catch (ExecutionException e) { - throw new IOException2(e); + throw new IOException(e); } } } @@ -2010,10 +2243,10 @@ public final class FilePath implements Serializable { * @return * number of files/directories that are written. */ - private static Integer writeToTar(File baseDir, DirScanner scanner, OutputStream out) throws IOException { + private Integer writeToTar(File baseDir, DirScanner scanner, OutputStream out) throws IOException { Archiver tw = ArchiverFactory.TAR.create(out); try { - scanner.scan(baseDir,tw); + scanner.scan(baseDir,reading(tw)); } finally { tw.close(); } @@ -2023,17 +2256,18 @@ public final class FilePath implements Serializable { /** * Reads from a tar stream and stores obtained files to the base dir. */ - private static void readFromTar(String name, File baseDir, InputStream in) throws IOException { + private void readFromTar(String name, File baseDir, InputStream in) throws IOException { TarInputStream t = new TarInputStream(in); try { TarEntry te; while ((te = t.getNextEntry()) != null) { File f = new File(baseDir,te.getName()); if(te.isDirectory()) { - f.mkdirs(); + mkdirs(f); } else { File parent = f.getParentFile(); - if (parent != null) parent.mkdirs(); + if (parent != null) mkdirs(parent); + writing(f); byte linkFlag = (Byte) LINKFLAG_FIELD.get(te); if (linkFlag==TarEntry.LF_SYMLINK) { @@ -2049,12 +2283,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(); } @@ -2072,7 +2306,7 @@ public final class FilePath implements Serializable { return new RemoteLauncher(listener,channel,channel.call(new IsUnix())); } - private static final class IsUnix implements Callable { + private static final class IsUnix extends MasterToSlaveCallable { public Boolean call() throws IOException { return File.pathSeparatorChar==':'; } @@ -2090,11 +2324,18 @@ public final class FilePath implements Serializable { * null if no error was found. Otherwise returns a human readable error message. * @since 1.90 * @see #validateFileMask(FilePath, String) + * @deprecated use {@link #validateAntFileMask(String, int)} instead */ public String validateAntFileMask(final String fileMasks) throws IOException, InterruptedException { return validateAntFileMask(fileMasks, Integer.MAX_VALUE); } + /** + * Default bound for {@link #validateAntFileMask(String, int)}. + * @since 1.592 + */ + public static int VALIDATE_ANT_FILE_MASK_BOUND = Integer.getInteger(FilePath.class.getName() + ".VALIDATE_ANT_FILE_MASK_BOUND", 10000); + /** * Like {@link #validateAntFileMask(String)} but performing only a bounded number of operations. *

Whereas the unbounded overload is appropriate for calling from cancelable, long-running tasks such as build steps, @@ -2104,12 +2345,12 @@ public final class FilePath implements Serializable { * A message is returned in case the file pattern can definitely be determined to not match anything in the directory within the alloted time. * If the time runs out without finding a match but without ruling out the possibility that there might be one, {@link InterruptedException} is thrown, * in which case the calling code should give the user the benefit of the doubt and use {@link hudson.util.FormValidation.Kind#OK} (with or without a message). - * @param bound a maximum number of negative operations (deliberately left vague) to perform before giving up on a precise answer; 10_000 is a reasonable pick + * @param bound a maximum number of negative operations (deliberately left vague) to perform before giving up on a precise answer; try {@link #VALIDATE_ANT_FILE_MASK_BOUND} * @throws InterruptedException not only in case of a channel failure, but also if too many operations were performed without finding any matches * @since 1.484 */ public String validateAntFileMask(final String fileMasks, final int bound) throws IOException, InterruptedException { - return act(new FileCallable() { + return act(new MasterToSlaveFileCallable() { private static final long serialVersionUID = 1; public String invoke(File dir, VirtualChannel channel) throws IOException, InterruptedException { if(fileMasks.startsWith("~")) @@ -2150,7 +2391,7 @@ public final class FilePath implements Serializable { {// check the (2) above next as this is more expensive. // Try prepending "**/" to see if that results in a match - FileSet fs = Util.createFileSet(dir,"**/"+fileMask); + FileSet fs = Util.createFileSet(reading(dir),"**/"+fileMask); DirectoryScanner ds = fs.getDirectoryScanner(new Project()); if(ds.getIncludedFilesCount()!=0) { // try shorter name first so that the suggestion results in least amount of changes @@ -2212,14 +2453,19 @@ public final class FilePath implements Serializable { class Cancel extends RuntimeException {} DirectoryScanner ds = bound == Integer.MAX_VALUE ? new DirectoryScanner() : new DirectoryScanner() { int ticks; + long start = System.currentTimeMillis(); @Override public synchronized boolean isCaseSensitive() { - if (!filesIncluded.isEmpty() || !dirsIncluded.isEmpty() || ticks++ > bound) { + if (!filesIncluded.isEmpty() || !dirsIncluded.isEmpty() || ticks++ > bound || System.currentTimeMillis() - start > 5000) { throw new Cancel(); } + filesNotIncluded.clear(); + dirsNotIncluded.clear(); + // notFollowedSymlinks might be large, but probably unusual + // scannedDirs will typically be largish, but seems to be needed return super.isCaseSensitive(); } }; - ds.setBasedir(dir); + ds.setBasedir(reading(dir)); ds.setIncludes(new String[] {pattern}); try { ds.scan(); @@ -2249,9 +2495,9 @@ public final class FilePath implements Serializable { /** * Shortcut for {@link #validateFileMask(String)} in case the left-hand side can be null. */ - public static FormValidation validateFileMask(FilePath pathOrNull, String value) throws IOException { - if(pathOrNull==null) return FormValidation.ok(); - return pathOrNull.validateFileMask(value); + public static FormValidation validateFileMask(@CheckForNull FilePath path, String value) throws IOException { + if(path==null) return FormValidation.ok(); + return path.validateFileMask(value); } /** @@ -2278,7 +2524,7 @@ public final class FilePath implements Serializable { if(!exists()) // no workspace. can't check return FormValidation.ok(); - String msg = validateAntFileMask(value, 10000); + String msg = validateAntFileMask(value, VALIDATE_ANT_FILE_MASK_BOUND); if(errorIfNotExist) return FormValidation.error(msg); else return FormValidation.warning(msg); } catch (InterruptedException e) { @@ -2365,7 +2611,7 @@ public final class FilePath implements Serializable { public VirtualChannel getChannel() { if(channel!=null) return channel; - else return Jenkins.MasterComputer.localChannel; + else return localChannel; } /** @@ -2392,8 +2638,14 @@ public final class FilePath implements Serializable { ois.defaultReadObject(); if(ois.readBoolean()) { this.channel = channel; + this.filter = null; } else { this.channel = null; + // If the remote channel wants us to create a FilePath that points to a local file, + // we need to make sure the access control takes place. + // This covers the immediate case of FileCallables taking FilePath into reference closure implicitly, + // but it also covers more general case of FilePath sent as a return value or argument. + this.filter = SoloFilePathFilter.wrap(FilePathFilter.current()); } } @@ -2428,6 +2680,14 @@ public final class FilePath implements Serializable { } } + /** + * Role check comes from {@link FileCallable}s. + */ + @Override + public void checkRoles(RoleChecker checker) throws SecurityException { + callable.checkRoles(checker); + } + public ClassLoader getClassLoader() { return classLoader; } @@ -2438,7 +2698,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); } @@ -2470,7 +2730,7 @@ public final class FilePath implements Serializable { * (User's home directory in the Unix sense) of the given channel. */ public static FilePath getHomeDirectory(VirtualChannel ch) throws InterruptedException, IOException { - return ch.call(new Callable() { + return ch.call(new MasterToSlaveCallable() { public FilePath call() throws IOException { return new FilePath(new File(System.getProperty("user.home"))); } @@ -2504,7 +2764,115 @@ public final class FilePath implements Serializable { scanSingle(new File(dir, workspacePath), archivedPath, visitor); } } + } + + 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); + + private @Nonnull SoloFilePathFilter filterNonNull() { + return filter!=null ? filter : UNRESTRICTED; + } + + /** + * Wraps {@link FileVisitor} to notify read access to {@link FilePathFilter}. + */ + private FileVisitor reading(final FileVisitor v) { + final FilePathFilter filter = FilePathFilter.current(); + if (filter==null) return v; + + return new FileVisitor() { + @Override + public void visit(File f, String relativePath) throws IOException { + filter.read(f); + v.visit(f,relativePath); + } + + @Override + public void visitSymlink(File link, String target, String relativePath) throws IOException { + filter.read(link); + v.visitSymlink(link, target, relativePath); + } + + @Override + public boolean understandsSymlink() { + return v.understandsSymlink(); + } + }; + } + + /** + * Pass through 'f' after ensuring that we can read that file. + */ + private File reading(File f) { + filterNonNull().read(f); + return f; + } + + /** + * Pass through 'f' after ensuring that we can access the file attributes. + */ + private File stating(File f) { + filterNonNull().stat(f); + return f; + } + + /** + * Pass through 'f' after ensuring that we can create that file/dir. + */ + private File creating(File f) { + filterNonNull().create(f); + return f; + } + + /** + * Pass through 'f' after ensuring that we can write to that file. + */ + private File writing(File f) { + FilePathFilter filter = filterNonNull(); + if (!f.exists()) + filter.create(f); + filter.write(f); + return f; + } + + /** + * Pass through 'f' after ensuring that we can create that symlink. + */ + private File symlinking(File f) { + FilePathFilter filter = filterNonNull(); + if (!f.exists()) + filter.create(f); + filter.symlink(f); + return f; + } + /** + * Pass through 'f' after ensuring that we can delete that file. + */ + private File deleting(File f) { + filterNonNull().delete(f); + return f; + } + + private boolean mkdirs(File dir) { + if (dir.exists()) return false; + + filterNonNull().mkdirs(dir); + return dir.mkdirs(); + } + + private File mkdirsE(File dir) throws IOException { + if (dir.exists()) { + return dir; + } + filterNonNull().mkdirs(dir); + return IOUtils.mkdirs(dir); } + private static final SoloFilePathFilter UNRESTRICTED = SoloFilePathFilter.wrap(FilePathFilter.UNRESTRICTED); } 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 53f53f2c52e92b99fa7f4106fbe6fd9380ebb3ac..b3fd046b105896514fc236c5a21da258305bf8d9 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; @@ -139,7 +138,7 @@ 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.jenkins.ui.icon.IconSet; import org.jvnet.tiger_types.Types; import org.kohsuke.stapler.Ancestor; import org.kohsuke.stapler.Stapler; @@ -149,9 +148,9 @@ 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; /** @@ -189,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) { @@ -205,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); @@ -225,6 +223,9 @@ public class Functions { */ context.setVariable("resURL",rootURL+getResourcePath()); context.setVariable("imagesURL",rootURL+getResourcePath()+"/images"); + + context.setVariable("userAgent", currentRequest.getHeader("User-Agent")); + IconSet.initPageVariables(context); } /** @@ -408,7 +409,7 @@ public class Functions { return Node.Mode.values(); } - public static String getProjectListString(List projects) { + public static String getProjectListString(List projects) { return Items.toNameList(projects); } @@ -445,13 +446,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; } /** @@ -508,6 +510,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. */ @@ -533,6 +544,20 @@ public class Functions { return map.subMap(Integer.parseInt(to),Integer.parseInt(from)-1); } + /** + * Creates a sub map by using the given range (upper end inclusive). + */ + @Restricted(NoExternalUse.class) + public static SortedMap filterExcludingFrom(SortedMap map, String from, String to) { + if(from==null && to==null) return map; + if(to==null) + return map.headMap(Integer.parseInt(from)); + if(from==null) + return map.tailMap(Integer.parseInt(to)); + + return map.subMap(Integer.parseInt(to),Integer.parseInt(from)); + } + private static final SimpleFormatter formatter = new SimpleFormatter(); /** @@ -580,6 +605,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, @@ -883,7 +912,7 @@ public class Functions { * @since 1.494 */ public static Collection getSortedDescriptorsForGlobalConfig(Predicate predicate) { - ExtensionList exts = Jenkins.getInstance().getExtensionList(Descriptor.class); + ExtensionList exts = ExtensionList.lookup(Descriptor.class); List r = new ArrayList(exts.size()); for (ExtensionComponent c : exts.getComponents()) { @@ -999,20 +1028,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 @@ -1125,7 +1149,6 @@ public class Functions { return sorted; } - @IgnoreJRERequirement public static ThreadInfo[] getThreadInfos() { ThreadMXBean mbean = ManagementFactory.getThreadMXBean(); return mbean.dumpAllThreads(mbean.isObjectMonitorUsageSupported(),mbean.isSynchronizerUsageSupported()); @@ -1189,20 +1212,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() + "\"" + @@ -1465,7 +1482,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") ; } diff --git a/core/src/main/java/hudson/Launcher.java b/core/src/main/java/hudson/Launcher.java index ba599e1753515becf9d52872405c0949558bcdd6..be003dfb648a76a3463bdae92151d9f812e37e0f 100644 --- a/core/src/main/java/hudson/Launcher.java +++ b/core/src/main/java/hudson/Launcher.java @@ -38,7 +38,10 @@ import hudson.remoting.VirtualChannel; import hudson.util.StreamCopyThread; import hudson.util.ArgumentListBuilder; import hudson.util.ProcessTree; +import jenkins.security.MasterToSlaveCallable; 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; @@ -144,6 +147,7 @@ public abstract class Launcher { public final class ProcStarter { protected List commands; protected boolean[] masks; + private boolean quiet; protected FilePath pwd; protected OutputStream stdout = NULL_OUTPUT_STREAM, stderr; protected InputStream stdin = NULL_INPUT_STREAM; @@ -210,6 +214,25 @@ public abstract class Launcher { return masks; } + /** + * Allows {@link #maskedPrintCommandLine(List, boolean[], FilePath)} to be suppressed from {@link hudson.Launcher.LocalLauncher#launch(hudson.Launcher.ProcStarter)}. + * Useful when the actual command being printed is noisy and unreadable and the caller would rather print diagnostic information in a customized way. + * @param quiet to suppress printing the command line when starting the process; false to keep default behavior of printing + * @return this + * @since 1.576 + */ + public ProcStarter quiet(boolean quiet) { + this.quiet = quiet; + return this; + } + + /** + * @since 1.576 + */ + public boolean quiet() { + return quiet; + } + public ProcStarter pwd(FilePath workDir) { this.pwd = workDir; return this; @@ -298,8 +321,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]; } /** @@ -364,7 +392,7 @@ public abstract class Launcher { * Copies a {@link ProcStarter}. */ public ProcStarter copy() { - ProcStarter rhs = new ProcStarter().cmds(commands).pwd(pwd).masks(masks).stdin(stdin).stdout(stdout).stderr(stderr).envs(envs); + ProcStarter rhs = new ProcStarter().cmds(commands).pwd(pwd).masks(masks).stdin(stdin).stdout(stdout).stderr(stderr).envs(envs).quiet(quiet); rhs.reverseStdin = this.reverseStdin; rhs.reverseStderr = this.reverseStderr; rhs.reverseStdout = this.reverseStdout; @@ -752,7 +780,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) { @@ -761,7 +789,9 @@ public abstract class Launcher { @Override public Proc launch(ProcStarter ps) throws IOException { - maskedPrintCommandLine(ps.commands, ps.masks, ps.pwd); + if (!ps.quiet) { + maskedPrintCommandLine(ps.commands, ps.masks, ps.pwd); + } EnvVars jobEnv = inherit(ps.envs); @@ -816,7 +846,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 +871,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. */ @@ -859,7 +913,7 @@ public abstract class Launcher { final String workDir = ps.pwd==null ? null : ps.pwd.getRemote(); try { - return new ProcImpl(getChannel().call(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, workDir, listener))); + return new ProcImpl(getChannel().call(new RemoteLaunchCallable(ps.commands, ps.masks, ps.envs, in, ps.reverseStdin, out, ps.reverseStdout, err, ps.reverseStderr, ps.quiet, workDir, listener))); } catch (InterruptedException e) { throw (IOException)new InterruptedIOException().initCause(e); } @@ -887,7 +941,7 @@ public abstract class Launcher { getChannel().call(new KillTask(modelEnvVars)); } - private static final class KillTask implements Callable { + private static final class KillTask extends MasterToSlaveCallable { private final Map modelEnvVars; public KillTask(Map modelEnvVars) { @@ -946,6 +1000,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 1.568 + */ + 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; @@ -962,7 +1098,7 @@ public abstract class Launcher { IOTriplet getIOtriplet(); } - private static class RemoteLaunchCallable implements Callable { + private static class RemoteLaunchCallable extends MasterToSlaveCallable { private final List cmd; private final boolean[] masks; private final String[] env; @@ -972,8 +1108,9 @@ public abstract class Launcher { private final String workDir; private final TaskListener listener; private final boolean reverseStdin, reverseStdout, reverseStderr; + private final boolean quiet; - RemoteLaunchCallable(List cmd, boolean[] masks, String[] env, InputStream in, boolean reverseStdin, OutputStream out, boolean reverseStdout, OutputStream err, boolean reverseStderr, String workDir, TaskListener listener) { + RemoteLaunchCallable(List cmd, boolean[] masks, String[] env, InputStream in, boolean reverseStdin, OutputStream out, boolean reverseStdout, OutputStream err, boolean reverseStderr, boolean quiet, String workDir, TaskListener listener) { this.cmd = new ArrayList(cmd); this.masks = masks; this.env = env; @@ -985,11 +1122,12 @@ public abstract class Launcher { this.reverseStdin = reverseStdin; this.reverseStdout = reverseStdout; this.reverseStderr = reverseStderr; + this.quiet = quiet; } public RemoteProcess call() throws IOException { Launcher.ProcStarter ps = new LocalLauncher(listener).launch(); - ps.cmds(cmd).masks(masks).envs(env).stdin(in).stdout(out).stderr(err); + ps.cmds(cmd).masks(masks).envs(env).stdin(in).stdout(out).stderr(err).quiet(quiet); if(workDir!=null) ps.pwd(workDir); if (reverseStdin) ps.writeStdin(); if (reverseStdout) ps.readStdout(); @@ -1032,7 +1170,7 @@ public abstract class Launcher { private static final long serialVersionUID = 1L; } - private static class RemoteChannelLaunchCallable implements Callable { + private static class RemoteChannelLaunchCallable extends MasterToSlaveCallable { private final String[] cmd; private final Pipe out; private final String workDir; diff --git a/core/src/main/java/hudson/LauncherDecorator.java b/core/src/main/java/hudson/LauncherDecorator.java index 0abfca30e2a9eb7da98fccab621ada8e5f5d2244..3bd5f7d0212b56bb87be7ebe56d1e5bda08c96c7 100644 --- a/core/src/main/java/hudson/LauncherDecorator.java +++ b/core/src/main/java/hudson/LauncherDecorator.java @@ -45,6 +45,6 @@ public abstract class LauncherDecorator implements ExtensionPoint { * Returns all the registered {@link LauncherDecorator}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(LauncherDecorator.class); + return ExtensionList.lookup(LauncherDecorator.class); } } 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/Plugin.java b/core/src/main/java/hudson/Plugin.java index 20f671e8dcc7e100c25cf0d0e67549691db0b30d..1e11d23bcd848cc1625542e564cbc7833228fa05 100644 --- a/core/src/main/java/hudson/Plugin.java +++ b/core/src/main/java/hudson/Plugin.java @@ -30,25 +30,28 @@ import hudson.model.Saveable; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SaveableListener; import hudson.model.Descriptor.FormException; -import org.kohsuke.stapler.MetaClass; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import javax.servlet.ServletContext; import javax.servlet.ServletException; -import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.File; -import java.net.URL; import net.sf.json.JSONObject; import com.thoughtworks.xstream.XStream; +import java.net.URI; +import java.net.URISyntaxException; +import org.kohsuke.stapler.HttpResponses; /** * Base class of Hudson plugin. * *

- * A plugin needs to derive from this class. + * A plugin may derive from this class, or it may directly define extension + * points annotated with {@link hudson.Extension}. For a list of extension + * points, see + * https://wiki.jenkins-ci.org/display/JENKINS/Extension+points. * *

* One instance of a plugin is created by Hudson, and used as the entry point @@ -201,15 +204,13 @@ public abstract class Plugin implements Saveable { public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { String path = req.getRestOfPath(); + if (path.startsWith("/META-INF/") || path.startsWith("/WEB-INF/")) { + throw HttpResponses.notFound(); + } + if(path.length()==0) path = "/"; - if(path.indexOf("..")!=-1 || path.length()<1) { - // don't serve anything other than files in the sub directory. - rsp.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - // Stapler routes requests like the "/static/.../foo/bar/zot" to be treated like "/foo/bar/zot" // and this is used to serve long expiration header, by using Jenkins.VERSION_HASH as "..." // to create unique URLs. Recognize that and set a long expiration header. @@ -219,7 +220,11 @@ public abstract class Plugin implements Saveable { long expires = staticLink ? TimeUnit2.DAYS.toMillis(365) : -1; // use serveLocalizedFile to support automatic locale selection - rsp.serveLocalizedFile(req, new URL(wrapper.baseResourceURL,'.'+path),expires); + try { + rsp.serveLocalizedFile(req, wrapper.baseResourceURL.toURI().resolve(new URI(null, '.' + path, null)).toURL(), expires); + } catch (URISyntaxException x) { + throw new IOException(x); + } } // diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 7ce9dea6d3456fbbab2306b7ad951265ae0215c8..d2918a7aa4b1c6c5ed307492bb2a41ce1eef6dd9 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -40,7 +40,7 @@ import hudson.security.Permission; import hudson.security.PermissionScope; import hudson.util.CyclicGraphDetector; import hudson.util.CyclicGraphDetector.CycleDetectedException; -import hudson.util.IOException2; +import hudson.util.IOUtils; import hudson.util.PersistedList; import hudson.util.Service; import hudson.util.VersionNumber; @@ -75,17 +75,19 @@ import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; +import javax.annotation.CheckForNull; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; +import java.io.Closeable; 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.net.URLClassLoader; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -103,6 +105,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Future; import java.util.jar.JarFile; +import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; import org.xml.sax.Attributes; @@ -110,7 +113,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. @@ -174,6 +181,11 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas */ private final PluginStrategy strategy; + /** + * Manifest of the plugin binaries that are bundled with core. + */ + private final Map bundledPluginManifests = new HashMap(); + public PluginManager(ServletContext context, File rootDir) { this.context = context; @@ -301,7 +313,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); @@ -405,16 +417,33 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas || bundledPlugins.contains(name.replaceAll("\\.jpi",".hpi")); } + /** + * Returns the manifest of a bundled but not-extracted plugin. + */ + public @CheckForNull Manifest getBundledPluginManifest(String shortName) { + return bundledPluginManifests.get(shortName); + } + /** * TODO: revisit where/how to expose this. This is an experiment. */ 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)); @@ -423,6 +452,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas plugins.add(p); activePlugins.add(p); + synchronized (((UberClassLoader) uberClassLoader).loaded) { + ((UberClassLoader) uberClassLoader).loaded.clear(); + } try { p.resolvePluginDependencies(); @@ -435,22 +467,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"); } @@ -490,6 +544,34 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas // - to make sure the value is not changed after each restart, so we can avoid // unpacking the plugin itself in ClassicPluginStrategy.explode } + if (pinFile.exists()) + parsePinnedBundledPluginManifest(src); + } + + /** + * When a pin file prevented a bundled plugin from getting extracted, check if the one we currently have + * is older than we bundled. + */ + private void parsePinnedBundledPluginManifest(URL bundledJpi) { + try { + URLClassLoader cl = new URLClassLoader(new URL[]{bundledJpi}); + InputStream in=null; + try { + URL res = cl.findResource(PluginWrapper.MANIFEST_FILENAME); + if (res!=null) { + in = res.openStream(); + Manifest manifest = new Manifest(in); + String shortName = PluginWrapper.computeShortName(manifest, FilenameUtils.getName(bundledJpi.getPath())); + bundledPluginManifests.put(shortName, manifest); + } + } finally { + IOUtils.closeQuietly(in); + if (cl instanceof Closeable) + ((Closeable)cl).close(); + } + } catch (IOException e) { + LOGGER.log(WARNING, "Failed to parse manifest of "+bundledJpi, e); + } } /** @@ -553,7 +635,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 +650,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 +664,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 +679,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 +770,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 +881,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 +1047,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 } @@ -941,8 +1063,8 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas * Keyed by the generated class name. */ private ConcurrentMap> generatedClasses = new ConcurrentHashMap>(); - - private ClassLoaderReflectionToolkit clt = new ClassLoaderReflectionToolkit(); + /** Cache of loaded, or known to be unloadable, classes. */ + private final Map> loaded = new HashMap>(); public UberClassLoader() { super(PluginManager.class.getClassLoader()); @@ -961,14 +1083,36 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas else generatedClasses.remove(name,wc); } + if (name.startsWith("SimpleTemplateScript")) { // cf. groovy.text.SimpleTemplateEngine + throw new ClassNotFoundException("ignoring " + name); + } + synchronized (loaded) { + if (loaded.containsKey(name)) { + Class c = loaded.get(name); + if (c != null) { + return c; + } else { + throw new ClassNotFoundException("cached miss for " + name); + } + } + } if (FAST_LOOKUP) { for (PluginWrapper p : activePlugins) { try { - Class c = clt.findLoadedClass(p.classLoader,name); - if (c!=null) return c; + Class c = ClassLoaderReflectionToolkit._findLoadedClass(p.classLoader, name); + if (c != null) { + synchronized (loaded) { + loaded.put(name, c); + } + return c; + } // calling findClass twice appears to cause LinkageError: duplicate class def - return clt.findClass(p.classLoader,name); - } catch (InvocationTargetException e) { + c = ClassLoaderReflectionToolkit._findClass(p.classLoader, name); + synchronized (loaded) { + loaded.put(name, c); + } + return c; + } catch (ClassNotFoundException e) { //not found. try next } } @@ -981,6 +1125,9 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas } } } + synchronized (loaded) { + loaded.put(name, null); + } // not found in any of the classloader. delegate. throw new ClassNotFoundException(name); } @@ -988,15 +1135,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 +1154,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))); @@ -1106,7 +1245,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas * @return this monitor. */ public static final PluginUpdateMonitor getInstance() { - return Jenkins.getInstance().getExtensionList(PluginUpdateMonitor.class).get(0); + return ExtensionList.lookup(PluginUpdateMonitor.class).get(0); } /** 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..aa683e630f7af7ca0306d86ad3474c2269584f1b 100644 --- a/core/src/main/java/hudson/PluginWrapper.java +++ b/core/src/main/java/hudson/PluginWrapper.java @@ -44,7 +44,8 @@ import java.util.List; import java.util.jar.Manifest; import java.util.logging.Logger; import static java.util.logging.Level.WARNING; - +import static org.apache.commons.io.FilenameUtils.getBaseName; +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 @@ -199,7 +202,7 @@ public class PluginWrapper implements Comparable, ModelObject { List dependencies, List optionalDependencies) { this.parent = parent; this.manifest = manifest; - this.shortName = computeShortName(manifest, archive); + this.shortName = computeShortName(manifest, archive.getName()); this.baseResourceURL = baseResourceURL; this.classLoader = classLoader; this.disableFile = disableFile; @@ -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, String fileName) { // 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"); @@ -247,19 +250,7 @@ public class PluginWrapper implements Comparable, ModelObject { // otherwise infer from the file name, since older plugins don't have // this entry. - return getBaseName(archive); - } - - - /** - * Gets the "abc" portion from "abc.ext". - */ - static String getBaseName(File archive) { - String n = archive.getName(); - int idx = n.lastIndexOf('.'); - if(idx>=0) - n = n.substring(0,idx); - return n; + return getBaseName(fileName); } @Exported @@ -283,8 +274,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; } /** @@ -338,6 +330,10 @@ public class PluginWrapper implements Comparable, ModelObject { */ @Exported public String getVersion() { + return getVersionOf(manifest); + } + + private String getVersionOf(Manifest manifest) { String v = manifest.getMainAttributes().getValue("Plugin-Version"); if(v!=null) return v; @@ -372,11 +368,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,13 +574,29 @@ 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 { return null; } } + + /** + * Checks if this plugin is pinned and that's forcing us to use an older version than the bundled one. + */ + public boolean isPinningForcingOldVersion() { + if (!isPinned()) return false; + + Manifest bundled = Jenkins.getInstance().pluginManager.getBundledPluginManifest(getShortName()); + if (bundled==null) return false; + + VersionNumber you = new VersionNumber(getVersionOf(bundled)); + VersionNumber me = getVersionNumber(); + + return me.isOlderThan(you); + } + // // // Action methods @@ -623,4 +640,8 @@ public class PluginWrapper implements Comparable, ModelObject { private static final Logger LOGGER = Logger.getLogger(PluginWrapper.class.getName()); + /** + * Name of the plugin manifest file (to help find where we parse them.) + */ + public static final String MANIFEST_FILENAME = "META-INF/MANIFEST.MF"; } 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/TcpSlaveAgentListener.java b/core/src/main/java/hudson/TcpSlaveAgentListener.java index 2c5f4f916c62591b4ddbb6f34da8833e8029324b..059e2af96566a291e3a9df0db5632687736d9f8b 100644 --- a/core/src/main/java/hudson/TcpSlaveAgentListener.java +++ b/core/src/main/java/hudson/TcpSlaveAgentListener.java @@ -32,8 +32,9 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.BindException; -import java.net.ServerSocket; +import java.net.InetSocketAddress; import java.net.Socket; +import java.nio.channels.ServerSocketChannel; import java.util.logging.Level; import java.util.logging.Logger; @@ -55,7 +56,7 @@ import java.util.logging.Logger; */ public final class TcpSlaveAgentListener extends Thread { - private final ServerSocket serverSocket; + private final ServerSocketChannel serverSocket; private volatile boolean shuttingDown; public final int configuredPort; @@ -67,13 +68,14 @@ public final class TcpSlaveAgentListener extends Thread { public TcpSlaveAgentListener(int port) throws IOException { super("TCP slave agent listener port="+port); try { - serverSocket = new ServerSocket(port); + serverSocket = ServerSocketChannel.open(); + serverSocket.socket().bind(new InetSocketAddress(port)); } catch (BindException e) { throw (BindException)new BindException("Failed to listen on port "+port+" because it's already in use.").initCause(e); } this.configuredPort = port; - LOGGER.info("JNLP slave agent listener started on TCP port "+getPort()); + LOGGER.log(Level.FINE, "JNLP slave agent listener started on TCP port {0}", getPort()); start(); } @@ -82,7 +84,7 @@ public final class TcpSlaveAgentListener extends Thread { * Gets the TCP port number in which we are listening. */ public int getPort() { - return serverSocket.getLocalPort(); + return serverSocket.socket().getLocalPort(); } @Override @@ -90,7 +92,7 @@ public final class TcpSlaveAgentListener extends Thread { try { // the loop eventually terminates when the socket is closed. while (true) { - Socket s = serverSocket.accept(); + Socket s = serverSocket.accept().socket(); // this prevents a connection from silently terminated by the router in between or the other peer // and that goes without unnoticed. However, the time out is often very long (for example 2 hours diff --git a/core/src/main/java/hudson/UDPBroadcastFragment.java b/core/src/main/java/hudson/UDPBroadcastFragment.java index 12ee1910705a589144235a72ba207f3d4d9df268..9e7039f739566cba48c4ad53001d67447cb75730 100644 --- a/core/src/main/java/hudson/UDPBroadcastFragment.java +++ b/core/src/main/java/hudson/UDPBroadcastFragment.java @@ -54,6 +54,6 @@ public abstract class UDPBroadcastFragment implements ExtensionPoint { * Returns all the registered {@link UDPBroadcastFragment}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(UDPBroadcastFragment.class); + return ExtensionList.lookup(UDPBroadcastFragment.class); } } diff --git a/core/src/main/java/hudson/URLConnectionDecorator.java b/core/src/main/java/hudson/URLConnectionDecorator.java index 04ec90657cfada7bf49b39c25c650e325f72f807..d6aad07b7081e76a3a2ab81b317ff1a22b2489f0 100644 --- a/core/src/main/java/hudson/URLConnectionDecorator.java +++ b/core/src/main/java/hudson/URLConnectionDecorator.java @@ -46,6 +46,6 @@ public abstract class URLConnectionDecorator implements ExtensionPoint { * Returns all the registered {@link URLConnectionDecorator}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(URLConnectionDecorator.class); + return ExtensionList.lookup(URLConnectionDecorator.class); } } diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index ad91672dc65a267e5a04bc0d3f4aee52d0642d90..7183937d2d9f0f91c84afcc6482d4a34f662b791 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -30,7 +30,6 @@ import edu.umd.cs.findbugs.annotations.SuppressWarnings; import hudson.Proc.LocalProc; import hudson.model.TaskListener; import hudson.os.PosixAPI; -import hudson.util.IOException2; import hudson.util.QuotedStringTokenizer; import hudson.util.VariableResolver; import hudson.util.jna.WinIOException; @@ -43,7 +42,6 @@ import org.apache.tools.ant.taskdefs.Copy; import org.apache.tools.ant.types.FileSet; import jnr.posix.FileStat; import jnr.posix.POSIX; -import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -64,6 +62,7 @@ import java.security.NoSuchAlgorithmException; import java.text.NumberFormat; import java.text.ParseException; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -74,6 +73,9 @@ import hudson.util.jna.Kernel32Utils; import static hudson.util.jna.GNUCLibrary.LIBC; import java.security.DigestInputStream; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.apache.commons.codec.digest.DigestUtils; /** @@ -95,7 +97,8 @@ public class Util { * Creates a filtered sublist. * @since 1.176 */ - public static 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') + buf.append(">"); + else if(ch=='&') buf.append("&"); else @@ -894,13 +925,17 @@ public class Util { return buf.toString(); } - public static String xmlEscape(String text) { + @Nonnull + public static String xmlEscape(@Nonnull String text) { StringBuilder buf = new StringBuilder(text.length()+64); for( int i=0; i') + buf.append(">"); + else if(ch=='&') buf.append("&"); else @@ -912,14 +947,14 @@ public class Util { /** * Creates an empty file. */ - public static void touch(File file) throws IOException { + public static void touch(@Nonnull File file) throws IOException { new FileOutputStream(file).close(); } /** * Copies a single file by using Ant. */ - public static void copyFile(File src, File dst) throws BuildException { + public static void copyFile(@Nonnull File src, @Nonnull File dst) throws BuildException { Copy cp = new Copy(); cp.setProject(new org.apache.tools.ant.Project()); cp.setTofile(dst); @@ -931,7 +966,8 @@ public class Util { /** * Convert null to "". */ - public static String fixNull(String s) { + @Nonnull + public static String fixNull(@CheckForNull String s) { if(s==null) return ""; else return s; } @@ -939,7 +975,8 @@ public class Util { /** * Convert empty string to null. */ - public static String fixEmpty(String s) { + @CheckForNull + public static String fixEmpty(@CheckForNull String s) { if(s==null || s.length()==0) return null; return s; } @@ -949,31 +986,37 @@ public class Util { * * @since 1.154 */ - public static String fixEmptyAndTrim(String s) { + @CheckForNull + public static String fixEmptyAndTrim(@CheckForNull String s) { if(s==null) return null; return fixEmpty(s.trim()); } - public static 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 +1029,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 +1044,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 +1072,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 +1095,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 +1112,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 +1185,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 +1255,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 +1276,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 +1362,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 +1376,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 +1392,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 +1407,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 +1417,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 +1431,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 +1443,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 +1453,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..11d438d5ab0496010403ef6e046d9684c8ca0a66 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,26 @@ 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.lang.reflect.Method; 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 +89,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 +106,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 @@ -110,22 +117,22 @@ public final class WebAppMain implements ServletContextListener { installLogger(); + markCookieAsHttpOnly(context); + 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 +170,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 +192,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 +202,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 +216,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 +243,64 @@ 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; } } + /** + * Set the session cookie as HTTP only. + * + * @see discussion of this topic in OWASP + */ + private void markCookieAsHttpOnly(ServletContext context) { + try { + Method m; + try { + m = context.getClass().getMethod("getSessionCookieConfig"); + } catch (NoSuchMethodException x) { // 3.0+ + LOGGER.log(Level.FINE, "Failed to set secure cookie flag", x); + return; + } + Object sessionCookieConfig = m.invoke(context); + + // not exposing session cookie to JavaScript to mitigate damage caused by XSS + Class scc = Class.forName("javax.servlet.SessionCookieConfig"); + Method setHttpOnly = scc.getMethod("setHttpOnly",boolean.class); + setHttpOnly.invoke(sessionCookieConfig,true); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to set HTTP-only cookie flag", 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/matrix/AxisDescriptor.java b/core/src/main/java/hudson/cli/AddJobToViewCommand.java similarity index 51% rename from core/src/main/java/hudson/matrix/AxisDescriptor.java rename to core/src/main/java/hudson/cli/AddJobToViewCommand.java index 7d93505e4e298db101b76beefb60fcd73e7d408f..cea483785a304ea5651758c4f078e8b8d2cb18b3 100644 --- a/core/src/main/java/hudson/matrix/AxisDescriptor.java +++ b/core/src/main/java/hudson/cli/AddJobToViewCommand.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2010, InfraDNA, Inc. + * 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 @@ -21,47 +21,48 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package hudson.matrix; +package hudson.cli; -import hudson.Util; -import hudson.model.Descriptor; -import hudson.model.Failure; -import jenkins.model.Jenkins; -import hudson.util.FormValidation; -import org.kohsuke.stapler.QueryParameter; +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; /** - * {@link Descriptor} for {@link Axis} - * - * @author Kohsuke Kawaguchi + * @author ogondza + * @since 1.570 */ -public abstract class AxisDescriptor extends Descriptor { - protected AxisDescriptor(Class clazz) { - super(clazz); - } +@Extension +public class AddJobToViewCommand extends CLICommand { - protected AxisDescriptor() { - } + @Argument(usage="Name of the view", required=true, index=0) + private View view; + + @Argument(usage="Job names", required=true, index=1) + private List jobs; - /** - * Return false if the user shouldn't be able to create thie axis from the UI. - */ - public boolean isInstantiable() { - return true; + @Override + public String getShortDescription() { + return Messages.AddJobToViewCommand_ShortDescription(); } - /** - * 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(); + @Override + protected int run() throws Exception { + view.checkPermission(View.CONFIGURE); - try { - Jenkins.checkGoodName(value); - return FormValidation.ok(); - } catch (Failure e) { - return FormValidation.error(e.getMessage()); + 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 dc2b636badee8a0801641d3f4890f8e8050c0aaa..39df01901ad1fe0a916afd00b704bed6cbf7c17f 100644 --- a/core/src/main/java/hudson/cli/BuildCommand.java +++ b/core/src/main/java/hudson/cli/BuildCommand.java @@ -37,6 +37,7 @@ import hudson.AbortException; import hudson.model.Item; import hudson.model.Result; import hudson.model.TaskListener; +import hudson.model.User; import hudson.model.queue.QueueTaskFuture; import hudson.scm.PollingResult.Change; import hudson.util.EditDistance; @@ -51,6 +52,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; @@ -101,15 +103,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, Util.fixNull(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,7 +126,11 @@ 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); @@ -150,6 +162,7 @@ public class BuildCommand extends CLICommand { } AbstractBuild b = f.waitForStart(); // wait for the start stdout.println("Started "+b.getFullDisplayName()); + stdout.flush(); if (sync || follow) { try { @@ -223,7 +236,9 @@ public class BuildCommand extends CLICommand { @Override public String getShortDescription() { - return Messages.BuildCommand_CLICause_ShortDescription(startedBy); + User user = User.get(startedBy, false); + String userName = user != null ? user.getDisplayName() : startedBy; + return Messages.BuildCommand_CLICause_ShortDescription(userName); } @Override diff --git a/core/src/main/java/hudson/cli/CLIAction.java b/core/src/main/java/hudson/cli/CLIAction.java index bb7a396da8600e6a712e8d38ec8f044929e938bd..3253fc2840074f4674e69d3f45456716f0692fcd 100644 --- a/core/src/main/java/hudson/cli/CLIAction.java +++ b/core/src/main/java/hudson/cli/CLIAction.java @@ -31,24 +31,29 @@ 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.model.RootAction; import hudson.remoting.Channel; /** + * Shows usage of CLI and commands. + * * @author ogondza */ @Extension @Restricted(NoExternalUse.class) -public class CLIAction implements RootAction { +public class CLIAction implements UnprotectedRootAction, StaplerProxy { private transient final Map duplexChannels = new HashMap(); @@ -62,8 +67,7 @@ public class CLIAction implements RootAction { } public String getUrlName() { - - return "/cli"; + return "cli"; } public void doCommand(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { @@ -82,42 +86,51 @@ public class CLIAction implements RootAction { req.getView(this, "command.jelly").forward(req, rsp); } - /** - * Handles HTTP requests for duplex channels for CLI. - */ - public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException { - final Jenkins jenkins = Jenkins.getInstance(); - if (!"POST".equals(req.getMethod())) { - // for GET request, serve _cli.jelly, assuming this is a browser - jenkins.checkPermission(Jenkins.READ); - req.setAttribute("command", CLICommand.clone("help")); - req.getView(this,"index.jelly").forward(req,rsp); - return; + @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; } + } - // 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.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)); - } - }); + /** + * Serves CLI-over-HTTP response. + */ + private class CliEndpointResponse extends HttpResponseException { + @Override + public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { try { - server.download(req,rsp); - } finally { - duplexChannels.remove(uuid); + // 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); } - } else { - duplexChannels.get(uuid).upload(req,rsp); } } } diff --git a/core/src/main/java/hudson/cli/CLICommand.java b/core/src/main/java/hudson/cli/CLICommand.java index 86c8aa04dc75e2421b64a00241fb22492fa04d72..2c68b0f6ff7ae094250e8dc89c14c81a8fad3280 100644 --- a/core/src/main/java/hudson/cli/CLICommand.java +++ b/core/src/main/java/hudson/cli/CLICommand.java @@ -36,7 +36,9 @@ import hudson.remoting.Channel; import hudson.remoting.ChannelProperty; import hudson.security.CliAuthenticator; import hudson.security.SecurityRealm; +import jenkins.security.MasterToSlaveCallable; import org.acegisecurity.Authentication; +import org.acegisecurity.BadCredentialsException; import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; import org.apache.commons.discovery.ResourceClassIterator; @@ -63,6 +65,7 @@ import java.nio.charset.Charset; import java.nio.charset.UnsupportedCharsetException; import java.util.List; import java.util.Locale; +import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; @@ -189,6 +192,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", @@ -238,6 +243,13 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { // signals an error without stack trace stderr.println(e.getMessage()); return -1; + } catch (BadCredentialsException e) { + // to the caller, we can't reveal whether the user didn't exist or the password didn't match. + // do that to the server log instead + String id = UUID.randomUUID().toString(); + LOGGER.log(Level.INFO, "CLI login attempt failed: "+id, e); + stderr.println("Bad Credentials. Search the server log for "+id+" for more details."); + return -1; } catch (Exception e) { e.printStackTrace(stderr); return -1; @@ -249,8 +261,8 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { /** * Get parser for this command. * - * Exposed to be overridden by {@link CLIRegisterer}. - * @since TODO + * Exposed to be overridden by {@link hudson.cli.declarative.CLIRegisterer}. + * @since 1.538 */ protected CmdLineParser getCmdLineParser() { return new CmdLineParser(this); @@ -398,7 +410,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { return checkChannel().call(new GetSystemProperty(name)); } - private static final class GetSystemProperty implements Callable { + private static final class GetSystemProperty extends MasterToSlaveCallable { private final String name; private GetSystemProperty(String name) { @@ -427,7 +439,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { } } - private static final class GetCharset implements Callable { + private static final class GetCharset extends MasterToSlaveCallable { public String call() throws IOException { return Charset.defaultCharset().name(); } @@ -442,7 +454,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { return checkChannel().call(new GetEnvironmentVariable(name)); } - private static final class GetEnvironmentVariable implements Callable { + private static final class GetEnvironmentVariable extends MasterToSlaveCallable { private final String name; private GetEnvironmentVariable(String name) { @@ -487,7 +499,7 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable { * Returns all the registered {@link CLICommand}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(CLICommand.class); + return ExtensionList.lookup(CLICommand.class); } /** 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/CliTransportAuthenticator.java b/core/src/main/java/hudson/cli/CliTransportAuthenticator.java index 514fee52f26ae520e59b105150d41b2d404323bb..0db90cce10c8a57cb7bb9b56b4d63060cf625afd 100644 --- a/core/src/main/java/hudson/cli/CliTransportAuthenticator.java +++ b/core/src/main/java/hudson/cli/CliTransportAuthenticator.java @@ -49,6 +49,6 @@ public abstract class CliTransportAuthenticator implements ExtensionPoint { public abstract void authenticate(String protocol, Channel channel, Connection con); public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(CliTransportAuthenticator.class); + return ExtensionList.lookup(CliTransportAuthenticator.class); } } diff --git a/core/src/main/java/hudson/cli/ClientAuthenticationCache.java b/core/src/main/java/hudson/cli/ClientAuthenticationCache.java index cd727e7ba42eb906aec2adf02d16ce0fe0587412..b00b816d37d6e0391a307584480dfeccb6da0983 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 jenkins.security.MasterToSlaveCallable; import org.acegisecurity.Authentication; import org.acegisecurity.AuthenticationException; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; @@ -14,6 +13,7 @@ import org.springframework.dao.DataAccessException; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.Properties; @@ -40,14 +40,23 @@ 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 MasterToSlaveCallable() { 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()) { - props.load(store.read()); + InputStream istream = store.read(); + try { + props.load(istream); + } finally { + istream.close(); + } } } diff --git a/core/src/main/java/hudson/cli/CommandDuringBuild.java b/core/src/main/java/hudson/cli/CommandDuringBuild.java index 6f9230b3eedfe531cadf21a315cbcd3fdff65388..dfa65f487ae5de60b985039fb204575a8d3b18c0 100644 --- a/core/src/main/java/hudson/cli/CommandDuringBuild.java +++ b/core/src/main/java/hudson/cli/CommandDuringBuild.java @@ -28,6 +28,7 @@ import jenkins.model.Jenkins; import hudson.model.Job; import hudson.model.Run; import hudson.remoting.Callable; +import jenkins.security.MasterToSlaveCallable; import org.kohsuke.args4j.CmdLineException; import java.io.IOException; @@ -67,6 +68,9 @@ public abstract class CommandDuringBuild extends CLICommand { try { Run r = j.getBuildByNumber(Integer.parseInt(envs[1])); if (r==null) throw new CmdLineException("No such build #"+envs[1]+" in "+envs[0]); + if (!r.isBuilding()) { + throw new CmdLineException(r + " is not currently being built"); + } return r; } catch (NumberFormatException e) { throw new CmdLineException("Invalid build number: "+envs[1]); @@ -81,7 +85,7 @@ public abstract class CommandDuringBuild extends CLICommand { /** * Gets the environment variables that points to the build being executed. */ - private static final class GetCharacteristicEnvironmentVariables implements Callable { + private static final class GetCharacteristicEnvironmentVariables extends MasterToSlaveCallable { public String[] call() throws IOException { return new String[] { System.getenv("JOB_NAME"), diff --git a/core/src/main/java/hudson/cli/ConsoleCommand.java b/core/src/main/java/hudson/cli/ConsoleCommand.java index 2f95da4fe2468cd6637d0ab7e9dce0e7fec9631e..5da97b0947b11846a0f282c7b9d0ca9685eba955 100644 --- a/core/src/main/java/hudson/cli/ConsoleCommand.java +++ b/core/src/main/java/hudson/cli/ConsoleCommand.java @@ -6,7 +6,6 @@ import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Item; import hudson.model.PermalinkProjectAction.Permalink; -import hudson.util.IOUtils; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.Option; @@ -16,6 +15,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintStream; +import org.apache.commons.io.IOUtils; /** * cat/tail/head of the console output. @@ -75,9 +75,13 @@ public class ConsoleCommand extends CLICommand { pos = logText.writeLogTo(pos, w); } while (!logText.isComplete()); } else { - InputStream in = run.getLogInputStream(); - IOUtils.skip(in,pos); - IOUtils.copy(new InputStreamReader(in,run.getCharset()),w); + InputStream logInputStream = run.getLogInputStream(); + try { + IOUtils.skip(logInputStream,pos); + org.apache.commons.io.IOUtils.copy(new InputStreamReader(logInputStream,run.getCharset()),w); + } finally { + logInputStream.close(); + } } } finally { w.flush(); // this pointless flush needed to work around SSHD-154 @@ -134,7 +138,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/GroovyshCommand.java b/core/src/main/java/hudson/cli/GroovyshCommand.java index 92a7d2731ee85051c7aa8c051ab5fc0678559286..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,11 +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 JENKINS-17929 this does not work // this being remote means no jline capability is available System.setProperty("jline.terminal", UnsupportedTerminal.class.getName()); @@ -122,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/InstallPluginCommand.java b/core/src/main/java/hudson/cli/InstallPluginCommand.java index 38c04b3996da3715f7fefc7314a537f28e3d8e7e..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; } diff --git a/core/src/main/java/hudson/cli/InstallToolCommand.java b/core/src/main/java/hudson/cli/InstallToolCommand.java index fb30efd0c08b52798e618e95f1dce303aefca908..7e0c37ac281beccff8a57a7db2c63dfaa5c6ab24 100644 --- a/core/src/main/java/hudson/cli/InstallToolCommand.java +++ b/core/src/main/java/hudson/cli/InstallToolCommand.java @@ -31,10 +31,7 @@ import hudson.model.AbstractProject; import hudson.model.Run; import hudson.model.Executor; import hudson.model.Node; -import hudson.model.EnvironmentSpecific; import hudson.model.Item; -import hudson.remoting.Callable; -import hudson.slaves.NodeSpecific; import hudson.util.EditDistance; import hudson.util.StreamTaskListener; import hudson.tools.ToolDescriptor; @@ -44,6 +41,7 @@ import java.util.List; import java.util.ArrayList; import java.io.IOException; +import jenkins.security.MasterToSlaveCallable; import org.kohsuke.args4j.Argument; /** @@ -122,13 +120,16 @@ 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()); return 0; } - private static final class BuildIDs implements Callable { + private static final class BuildIDs extends MasterToSlaveCallable { String job,number,id; public BuildIDs call() throws IOException { diff --git a/core/src/main/java/hudson/tasks/test/TestResultAggregator.java b/core/src/main/java/hudson/cli/RemoveJobFromViewCommand.java similarity index 51% rename from core/src/main/java/hudson/tasks/test/TestResultAggregator.java rename to core/src/main/java/hudson/cli/RemoveJobFromViewCommand.java index 22d90b9134727dbe5bab7bbb260e320e4df2e373..8c317d908f97bccfe39c14f8434a2167e1814249 100644 --- a/core/src/main/java/hudson/tasks/test/TestResultAggregator.java +++ b/core/src/main/java/hudson/cli/RemoveJobFromViewCommand.java @@ -1,18 +1,18 @@ /* * The MIT License - * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo!, Inc. - * + * + * 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 @@ -21,40 +21,48 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package hudson.tasks.test; +package hudson.cli; -import hudson.Launcher; -import hudson.matrix.MatrixAggregator; -import hudson.matrix.MatrixBuild; -import hudson.matrix.MatrixRun; -import hudson.model.BuildListener; +import java.util.List; -import java.io.IOException; +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; /** - * Aggregates {@link AbstractTestResultAction}s of {@link MatrixRun}s - * into {@link MatrixBuild}. - * - * @author Kohsuke Kawaguchi + * @author ogondza + * @since 1.570 */ -public class TestResultAggregator extends MatrixAggregator { - private MatrixTestResult result; +@Extension +public class RemoveJobFromViewCommand extends CLICommand { - public TestResultAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) { - super(build, launcher, listener); - } + @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 boolean startBuild() throws InterruptedException, IOException { - result = new MatrixTestResult(build); - build.addAction(result); - return true; + public String getShortDescription() { + return Messages.RemoveJobFromViewCommand_ShortDescription(); } @Override - public boolean endRun(MatrixRun run) throws InterruptedException, IOException { - AbstractTestResultAction atr = run.getAction(AbstractTestResultAction.class); - if(atr!=null) result.add(atr); - return true; + 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/SetBuildParameterCommand.java b/core/src/main/java/hudson/cli/SetBuildParameterCommand.java index d2c976e5e7aa14f3a6d031511b22178a108e579c..a54f0173e6325566f3e19fb193d26c6a8cfc9af5 100644 --- a/core/src/main/java/hudson/cli/SetBuildParameterCommand.java +++ b/core/src/main/java/hudson/cli/SetBuildParameterCommand.java @@ -32,14 +32,13 @@ public class SetBuildParameterCommand extends CommandDuringBuild { @Override protected int run() throws Exception { Run r = getCurrentlyBuilding(); + r.checkPermission(Run.UPDATE); StringParameterValue p = new StringParameterValue(name, value); 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/SetBuildResultCommand.java b/core/src/main/java/hudson/cli/SetBuildResultCommand.java index 2b6b2c60d693f21345390bfe696cbdab6a5b2993..7666d590a5f7147f33b08f7b15b4a08235e3800a 100644 --- a/core/src/main/java/hudson/cli/SetBuildResultCommand.java +++ b/core/src/main/java/hudson/cli/SetBuildResultCommand.java @@ -48,7 +48,7 @@ public class SetBuildResultCommand extends CommandDuringBuild { @Override protected int run() throws Exception { Run r = getCurrentlyBuilding(); - r.getParent().checkPermission(Item.BUILD); + r.checkPermission(Run.UPDATE); r.setResult(result); 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 cb1d1db8f2386c23f3f10a8c7b7cd2ee2e97de57..fe7ed1f690b8fa4aa486a2cc0ae9cd689fa91ac4 100644 --- a/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java +++ b/core/src/main/java/hudson/cli/declarative/CLIRegisterer.java @@ -149,8 +149,6 @@ public class CLIRegisterer extends ExtensionFinder { while (!chains.isEmpty()) binders.add(new MethodBinder(chains.pop(),this,parser)); - new ClassParser().parse(Jenkins.getInstance().getSecurityRealm().createCliAuthenticator(this), parser); - return parser; } @@ -169,6 +167,7 @@ public class CLIRegisterer extends ExtensionFinder { try { // 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/GenericItemOptionHandler.java b/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java index 502d65f9254e21b31653fd17334370cefdf27c00..e27bb32dc8709b8972c64cd7db80b24b0c61634f 100644 --- a/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java +++ b/core/src/main/java/hudson/cli/handlers/GenericItemOptionHandler.java @@ -26,7 +26,11 @@ 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; @@ -43,6 +47,8 @@ import org.kohsuke.args4j.spi.Setter; */ 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); } @@ -50,10 +56,21 @@ public abstract class GenericItemOptionHandler extends OptionHan protected abstract Class type(); @Override public int parseArguments(Parameters params) throws CmdLineException { - Jenkins j = Jenkins.getInstance(); - String src = params.getParameter(0); + 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() + "'?"); diff --git a/core/src/main/java/hudson/cli/handlers/ViewOptionHandler.java b/core/src/main/java/hudson/cli/handlers/ViewOptionHandler.java index 8e9373a3b74d2a60c31c5483d27dd8cbde09ce86..387621ae56b570e70731cbf7f6a6f6a98e755d41 100644 --- a/core/src/main/java/hudson/cli/handlers/ViewOptionHandler.java +++ b/core/src/main/java/hudson/cli/handlers/ViewOptionHandler.java @@ -53,7 +53,7 @@ import org.kohsuke.args4j.spi.Setter; * 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. + * name does not exist or a user was not granted {@link View#READ} permission. * * @author ogondza * @since 1.538 diff --git a/core/src/main/java/hudson/cli/util/ScriptLoader.java b/core/src/main/java/hudson/cli/util/ScriptLoader.java index 08df6f6d68b2f7d965dafa3c4ce69e57a596169f..e2252efbd5f78f9670bd272e6fa130180e78b3ce 100644 --- a/core/src/main/java/hudson/cli/util/ScriptLoader.java +++ b/core/src/main/java/hudson/cli/util/ScriptLoader.java @@ -1,7 +1,9 @@ package hudson.cli.util; import hudson.AbortException; -import hudson.remoting.Callable; +import jenkins.security.MasterToSlaveCallable; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import java.io.File; import java.io.IOException; @@ -9,15 +11,12 @@ import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; - /** - * - * @author vjuranek + * Reads a file (either a path or URL) over a channel. * + * @author vjuranek */ -public class ScriptLoader implements Callable { +public class ScriptLoader extends MasterToSlaveCallable { private final String script; @@ -43,5 +42,4 @@ public class ScriptLoader implements Callable { s.close(); } } - } diff --git a/core/src/main/java/hudson/console/AnnotatedLargeText.java b/core/src/main/java/hudson/console/AnnotatedLargeText.java index 298e034bf15ebf31a8f728b5c6bb9e7a6a82946b..89e307bf22df8a46ad4e97dc0acc09bda283eedf 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()); @@ -148,11 +145,24 @@ public class AnnotatedLargeText extends LargeText { return super.writeLogTo(start,w); } + /** + * Strips annotations using a {@link PlainTextConsoleOutputStream}. + * @inheritDoc + */ @Override public long writeLogTo(long start, OutputStream out) throws IOException { return super.writeLogTo(start, new PlainTextConsoleOutputStream(out)); } + /** + * Calls {@link LargeText#writeLogTo(long, OutputStream)} without stripping annotations as {@link #writeLogTo(long, OutputStream)} would. + * @inheritDoc + * @since 1.577 + */ + public long writeRawLogTo(long start, OutputStream out) throws IOException { + return super.writeLogTo(start, out); + } + public long writeHtmlTo(long start, Writer w) throws IOException { ConsoleAnnotationOutputStream caw = new ConsoleAnnotationOutputStream( w, createAnnotator(Stapler.getCurrentRequest()), context, charset); diff --git a/core/src/main/java/hudson/console/ConsoleAnnotatorFactory.java b/core/src/main/java/hudson/console/ConsoleAnnotatorFactory.java index 522b23f4ec023d1bea631c94ae0bd28487c7191e..b4e63de84a60ec4c0e9ca4837827ba16d551627a 100644 --- a/core/src/main/java/hudson/console/ConsoleAnnotatorFactory.java +++ b/core/src/main/java/hudson/console/ConsoleAnnotatorFactory.java @@ -127,6 +127,6 @@ public abstract class ConsoleAnnotatorFactory implements ExtensionPoint { * All the registered instances. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(ConsoleAnnotatorFactory.class); + return ExtensionList.lookup(ConsoleAnnotatorFactory.class); } } diff --git a/core/src/main/java/hudson/console/ConsoleLogFilter.java b/core/src/main/java/hudson/console/ConsoleLogFilter.java index 91654ab816aeab192916af58ec7a29eaf4d6017c..c980c1e1fbfceba7d557bf921820c7a7c5c5a775 100644 --- a/core/src/main/java/hudson/console/ConsoleLogFilter.java +++ b/core/src/main/java/hudson/console/ConsoleLogFilter.java @@ -55,6 +55,6 @@ public abstract class ConsoleLogFilter implements ExtensionPoint { * All the registered {@link ConsoleLogFilter}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(ConsoleLogFilter.class); + return ExtensionList.lookup(ConsoleLogFilter.class); } } 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..bb4e8e50401e9292932cf948995d4bb21029ebcb 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. @@ -129,7 +134,7 @@ public final class HudsonHomeDiskUsageMonitor extends AdministrativeMonitor { * All registered {@link Solution}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(Solution.class); + return ExtensionList.lookup(Solution.class); } } } diff --git a/core/src/main/java/hudson/diagnosis/NullIdDescriptorMonitor.java b/core/src/main/java/hudson/diagnosis/NullIdDescriptorMonitor.java index 8892fb1f2ab98aadda857d19f5654ffc385f7875..3182ddc9542fc8b260e0c4322d49d1438c0dc90b 100644 --- a/core/src/main/java/hudson/diagnosis/NullIdDescriptorMonitor.java +++ b/core/src/main/java/hudson/diagnosis/NullIdDescriptorMonitor.java @@ -62,6 +62,9 @@ public class NullIdDescriptorMonitor extends AdministrativeMonitor { private void verify() { Jenkins h = Jenkins.getInstance(); + if (h == null) { + return; + } for (Descriptor d : h.getExtensionList(Descriptor.class)) { PluginWrapper p = h.getPluginManager().whichPlugin(d.getClass()); String id; diff --git a/core/src/main/java/hudson/diagnosis/OldDataMonitor.java b/core/src/main/java/hudson/diagnosis/OldDataMonitor.java index bb05a4b48541c6fd125631ca4ffc9a9581d62f34..f2e1b1eaa14c330bd89418f998bd69f3b7dc0e37 100644 --- a/core/src/main/java/hudson/diagnosis/OldDataMonitor.java +++ b/core/src/main/java/hudson/diagnosis/OldDataMonitor.java @@ -23,13 +23,14 @@ */ package hudson.diagnosis; +import com.google.common.base.Predicate; +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 +38,24 @@ 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.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.List; 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,10 +65,13 @@ 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 boolean updating = false; + private HashMap data = new HashMap(); + + static OldDataMonitor get(Jenkins j) { + return (OldDataMonitor) j.getAdministrativeMonitor("OldData"); + } public OldDataMonitor() { super("OldData"); @@ -80,18 +86,28 @@ public class OldDataMonitor extends AdministrativeMonitor { return !data.isEmpty(); } - public synchronized Map getData() { - return Collections.unmodifiableMap(data); + public Map getData() { + Map _data; + synchronized (this) { + _data = new HashMap(this.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 +145,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); } @@ -183,11 +200,12 @@ public class OldDataMonitor extends AdministrativeMonitor { } return; } - OldDataMonitor odm = (OldDataMonitor) j.getAdministrativeMonitor("OldData"); + 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())); } } @@ -242,6 +260,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); @@ -255,19 +274,19 @@ 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 { - 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(); - VersionNumber version = entry.getValue().max; - if (version != null && (thruVer == null || !version.isNewerThan(thruVer))) { - entry.getKey().save(); - it.remove(); + @RequirePOST + public HttpResponse doUpgrade(StaplerRequest req, StaplerResponse rsp) { + final String thruVerParam = req.getParameter("thruVer"); + final VersionNumber thruVer = thruVerParam.equals("all") ? null : new VersionNumber(thruVerParam); + + saveAndRemoveEntries( new Predicate>() { + @Override + public boolean apply(Map.Entry entry) { + VersionNumber version = entry.getValue().max; + return version != null && (thruVer == null || !version.isNewerThan(thruVer)); } - } - updating = false; + }); + return HttpResponses.forwardToPreviousPage(); } @@ -275,23 +294,113 @@ 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 { - updating = true; - for (Iterator> it = data.entrySet().iterator(); it.hasNext();) { - Map.Entry entry = it.next(); - if (entry.getValue().max == null) { - entry.getKey().save(); - it.remove(); + @RequirePOST + public HttpResponse doDiscard(StaplerRequest req, StaplerResponse rsp) { + saveAndRemoveEntries( new Predicate>() { + @Override + public boolean apply(Map.Entry entry) { + return entry.getValue().max == null; } - } - updating = false; + }); + return HttpResponses.forwardToPreviousPage(); } + private void saveAndRemoveEntries(Predicate> matchingPredicate) { + /* + * Note that there a race condition here: we acquire the lock and get localCopy which includes some + * project (say); then we go through our loop and save that project; then someone POSTs a new + * config.xml for the project with some old data, causing remove to be called and the project to be + * added to data (in the new version); then we hit the end of this method and the project is removed + * from data again, even though it again has old data. + * + * In practice this condition is extremely unlikely, and not a major problem even if it + * does occur: just means the user will be prompted to discard less than they should have been (and + * would see the warning again after next restart). + */ + Map localCopy = null; + synchronized (this) { + localCopy = new HashMap(data); + } + + List removed = new ArrayList(); + for (Map.Entry entry : localCopy.entrySet()) { + if (matchingPredicate.apply(entry)) { + Saveable s = entry.getKey().get(); + if (s != null) { + try { + s.save(); + } catch (Exception x) { + LOGGER.log(Level.WARNING, "failed to save " + s, x); + } + } + removed.add(entry.getKey()); + } + } + + synchronized (this) { + data.keySet().removeAll(removed); + } + } + 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..94412637bfcd20a22c138bafd6853c8086b11606 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.startsWith(inferred)) { // not using equals due to JENKINS-24014 + 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/InitStrategy.java b/core/src/main/java/hudson/init/InitStrategy.java index 9012b4c65a3df81baf0be07fc9374b5770de8cce..7b8497405d4ef6014b6b01076a770f5538e9c63d 100644 --- a/core/src/main/java/hudson/init/InitStrategy.java +++ b/core/src/main/java/hudson/init/InitStrategy.java @@ -10,6 +10,7 @@ import java.util.Collection; import java.util.List; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; import java.util.logging.Level; import java.util.logging.Logger; @@ -49,6 +50,15 @@ public class InitStrategy { // for example, while doing "mvn jpi:run" or "mvn hpi:run" on a plugin that's bundled with Jenkins, we want to the // *.jpl file to override the bundled jpi/hpi file. getBundledPluginsFromProperty(r); + Iterator it = r.iterator(); + while (it.hasNext()) { + File f = it.next(); + if (new File(pm.rootDir, f.getName().replace(".hpi", ".jpi") + ".pinned").isFile()) { + // Cf. PluginManager.copyBundledPlugin, which is not called in this case. + LOGGER.log(Level.INFO, "ignoring {0} since this plugin is pinned", f); + it.remove(); + } + } // similarly, we prefer *.jpi over *.hpi listPluginFiles(pm, ".jpl", r); // linked plugin. for debugging. 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/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..81c9486e3fa8fd0f6ce3d4b9c4ec111faaf5e06b 100644 --- a/core/src/main/java/hudson/logging/LogRecorder.java +++ b/core/src/main/java/hudson/logging/LogRecorder.java @@ -29,11 +29,9 @@ import hudson.Extension; import hudson.FilePath; import hudson.Util; import hudson.XmlFile; -import hudson.model.AbstractModelObject; -import hudson.model.Computer; +import hudson.model.*; +import hudson.util.HttpResponses; import jenkins.model.Jenkins; -import hudson.model.Saveable; -import hudson.model.TaskListener; import hudson.model.listeners.SaveableListener; import hudson.remoting.Callable; import hudson.remoting.Channel; @@ -42,25 +40,18 @@ import hudson.slaves.ComputerListener; import hudson.util.CopyOnWriteList; import hudson.util.RingBufferLogHandler; import hudson.util.XStream2; +import jenkins.security.MasterToSlaveCallable; import net.sf.json.JSONObject; -import org.kohsuke.stapler.DataBoundConstructor; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.StaplerResponse; +import org.kohsuke.stapler.*; import org.kohsuke.stapler.interceptor.RequirePOST; import javax.servlet.ServletException; import java.io.File; import java.io.IOException; import java.text.Collator; -import java.util.ArrayList; -import java.util.List; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; +import java.util.*; import java.util.logging.Level; +import java.util.logging.LogManager; import java.util.logging.LogRecord; import java.util.logging.Logger; import org.kohsuke.accmod.Restricted; @@ -84,14 +75,51 @@ 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) + public AutoCompletionCandidates doAutoCompleteLoggerName(@QueryParameter String value) { + AutoCompletionCandidates candidates = new AutoCompletionCandidates(); + Enumeration loggerNames = LogManager.getLogManager().getLoggerNames(); + while (loggerNames.hasMoreElements()) { + String loggerName = loggerNames.nextElement(); + if (loggerName.toLowerCase(Locale.ENGLISH).contains(value.toLowerCase(Locale.ENGLISH))) { + candidates.add(loggerName); + } + } + return candidates; + } + + @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 +151,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 +169,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); @@ -160,7 +208,7 @@ public class LogRecorder extends AbstractModelObject implements Saveable { } - private static final class SetLevel implements Callable { + private static final class SetLevel extends MasterToSlaveCallable { /** known loggers (kept per slave), to avoid GC */ @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private static final Set loggers = new HashSet(); private final String name; @@ -253,6 +301,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 +353,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/matrix/JDKAxis.java b/core/src/main/java/hudson/markup/EscapedMarkupFormatter.java similarity index 58% rename from core/src/main/java/hudson/matrix/JDKAxis.java rename to core/src/main/java/hudson/markup/EscapedMarkupFormatter.java index fecd908deef180466e44601dbf2ce40e247bfb04..f106b5de2c6f00520a0656420c780143f0cb0f33 100644 --- a/core/src/main/java/hudson/matrix/JDKAxis.java +++ b/core/src/main/java/hudson/markup/EscapedMarkupFormatter.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2010, InfraDNA, Inc. + * 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 @@ -21,52 +21,40 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package hudson.matrix; +package hudson.markup; import hudson.Extension; -import jenkins.model.Jenkins; +import hudson.Util; +import hudson.markup.MarkupFormatter; +import hudson.markup.MarkupFormatterDescriptor; +import java.io.IOException; +import java.io.Writer; import org.kohsuke.stapler.DataBoundConstructor; -import java.util.Arrays; -import java.util.List; - /** - * {@link Axis} that selects available JDKs. + * @link MarkupFormatter} that treats the input as the escaped html. * - * @author Kohsuke Kawaguchi + * @author Seiji Sogabe + * @since 1.553 */ -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); - } +public class EscapedMarkupFormatter extends MarkupFormatter { @DataBoundConstructor - public JDKAxis(String[] values) { - super("jdk", Arrays.asList(values)); + public EscapedMarkupFormatter() { } @Override - public boolean isSystem() { - return true; + public void translate(String markup, Writer output) throws IOException { + output.write(Util.escape(markup)); } @Extension - public static class DescriptorImpl extends AxisDescriptor { - @Override - public String getDisplayName() { - return Messages.JDKAxis_DisplayName(); - } + public static class DescriptorImpl extends MarkupFormatterDescriptor { - /** - * If there's no JDK configured, there's no point in this axis. - */ @Override - public boolean isInstantiable() { - return !Jenkins.getInstance().getJDKs().isEmpty(); + 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/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 bcbdf18f96bba7ac64c506cbd37a140bf81835a7..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/Combination.java +++ /dev/null @@ -1,266 +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 groovy.lang.Binding; -import hudson.Util; -import hudson.matrix.MatrixBuild.MatrixBuildExecution; - -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; - -/** - * 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; - } - - /** - * 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 - * @deprecated as of 1.528 - * Use {@link FilterScript#apply(MatrixBuildExecution, Combination)} - */ - public boolean evalGroovyExpression(AxisList axes, String expression, Binding binding) { - return FilterScript.parse(expression).apply(axes, this, binding); - } - - 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/DefaultMatrixExecutionStrategyImpl.java b/core/src/main/java/hudson/matrix/DefaultMatrixExecutionStrategyImpl.java deleted file mode 100644 index d1ee9f0a92f341277ffd8f037dd9942dd1f31774..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/DefaultMatrixExecutionStrategyImpl.java +++ /dev/null @@ -1,301 +0,0 @@ -package hudson.matrix; - -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.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 FilterScript combinationFilter = FilterScript.parse(execution.getProject().getCombinationFilter()); - final FilterScript touchStoneFilter = FilterScript.parse(getTouchStoneCombinationFilter()); - - try { - - for (MatrixConfiguration c: execution.getActiveConfigurations()) { - - if (!MatrixBuildListener.buildConfiguration(build, c)) continue; // skip rebuild - - final Combination combination = c.getCombination(); - - if (touchStoneFilter != null && touchStoneFilter.apply(execution, combination)) { - touchStoneConfigurations.add(c); - } else if (combinationFilter.apply(execution, combination)) { - 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 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/FilterScript.java b/core/src/main/java/hudson/matrix/FilterScript.java deleted file mode 100644 index 1668ce0d3a20c42ec4a6a3d6bc8530c4ce29bd53..0000000000000000000000000000000000000000 --- a/core/src/main/java/hudson/matrix/FilterScript.java +++ /dev/null @@ -1,115 +0,0 @@ -package hudson.matrix; - -import groovy.lang.Binding; -import groovy.lang.GroovyShell; -import groovy.lang.Script; -import hudson.Util; -import hudson.matrix.Combination.BooleanCategory; -import hudson.matrix.MatrixBuild.MatrixBuildExecution; -import hudson.model.ParameterValue; -import hudson.model.ParametersAction; - -import java.util.Map; - -import static java.lang.Boolean.*; - -/** - * Groovy filter script that accepts or rejects matrix {@link Combination}. - * - * Instances of this class is thread unsafe. - * - * @author Kohsuke Kawaguchi - */ -class FilterScript { - private final Script script; - - FilterScript(Script script) { - this.script = script; - } - - /** - * @param context - * Variables the script will see. - */ - boolean evaluate(Binding context) { - script.setBinding(context); - return TRUE.equals(script.run()); - } - - /** - * Obtains a number N such that "N%M==0" would create - * a reasonable sparse matrix for integer M. - * - *

- * This is bit like {@link Combination#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, Combination c) { - long r = 0; - for (Axis a : axis) { - r += a.indexOf(c.get(a)); - r *= 31; - } - return r; - } - - /** - * Applies the filter to the specified combination in the context of {@code context}. - */ - public boolean apply(MatrixBuildExecution context, Combination combination) { - return apply(context.getProject().getAxes(), combination, getConfiguredBinding(context)); - } - - /*package*/ boolean apply(AxisList axes, Combination c, Binding binding) { - for (Map.Entry e : c.entrySet()) - binding.setVariable(e.getKey(),e.getValue()); - - binding.setVariable("index",toModuloIndex(axes,c)); - binding.setVariable("uniqueId", c.toIndex(axes)); - - return evaluate(binding); - } - - 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; - } - - public static FilterScript parse(String expression) { - if (Util.fixEmptyAndTrim(expression)==null) - return NOOP; - - GroovyShell shell = new GroovyShell(); - - return new FilterScript(shell.parse("use("+BooleanCategory.class.getName().replace('$','.')+") {"+expression+"}")); - } - - /** - * Constant that always applies to any combination. - */ - private static final FilterScript NOOP = new FilterScript(new Script() { - @Override - public Object run() { - return true; - } - }) { - @Override - public boolean apply(MatrixBuildExecution context, Combination combination) { - return true; - } - }; -} 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/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/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 bbc7f54198446d4a353402b494315be20515306b..28ef3ff5e83c985d5da9b9b8a2018d298589871d 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; @@ -54,12 +54,8 @@ import hudson.tasks.BuildWrapper; import hudson.tasks.Builder; import hudson.tasks.Fingerprinter.FingerprintAction; import hudson.tasks.Publisher; -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 +69,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 +83,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 +101,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,22 +157,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} (or {@link #none}) of adjacent builds. - */ - private volatile transient BuildReference previousBuild, nextBuild; - - @SuppressWarnings({"unchecked", "rawtypes"}) private static final BuildReference NONE = new BuildReference("NONE", null); - @SuppressWarnings("unchecked") private BuildReference none() {return NONE;} - - /*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); @@ -189,80 +179,26 @@ public abstract class AbstractBuild

,R extends Abs return getParent(); } - @Override - void dropLinks() { - super.dropLinks(); + @Override public final LazyBuildMixIn.RunMixIn getRunMixIn() { + return runMixIn; + } - if(nextBuild!=null) { - AbstractBuild nb = nextBuild.get(); - if (nb!=null) { - nb.previousBuild = previousBuild; - } - } - if(previousBuild!=null) { - AbstractBuild pb = previousBuild.get(); - if (pb!=null) pb.nextBuild = nextBuild; - } + @Override protected final BuildReference createReference() { + return getRunMixIn().createReference(); + } + + @Override protected final void dropLinks() { + getRunMixIn().dropLinks(); } @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.previousBuild = none(); - return null; - } - } - if (r==none()) - 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.previousBuild = null; - } + 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.nextBuild = none(); - return null; - } - } - if (r==none()) - 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(); } /** @@ -356,7 +292,7 @@ public abstract class AbstractBuild

,R extends Abs * Normally, a workspace is assigned by {@link hudson.model.Run.RunExecution}, but this lets you set the workspace in case * {@link AbstractBuild} is created without a build. */ - protected void setWorkspace(FilePath ws) { + protected void setWorkspace(@Nonnull FilePath ws) { this.workspace = ws.getRemote(); } @@ -416,8 +352,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()); @@ -495,11 +431,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() { @@ -518,7 +470,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. @@ -528,72 +480,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); - getProject().getScmCheckoutStrategy().preCheckout(AbstractBuild.this, launcher, this.listener); - getProject().getScmCheckoutStrategy().checkout(this); + for (WorkspaceListener wl : WorkspaceListener.all()) { + wl.beforeUse(AbstractBuild.this, lease.path, listener); + } - if (!preBuild(listener,project.getProperties())) - return Result.FAILURE; + getProject().getScmCheckoutStrategy().preCheckout(AbstractBuild.this, launcher, this.listener); + getProject().getScmCheckoutStrategy().checkout(this); - Result result = doRun(listener); + if (!preBuild(listener,project.getProperties())) + return Result.FAILURE; - 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."); + Result result = doRun(listener); - 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); + 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."); - listener.getLogger().print(ExpandableDetailsNote.encodeTo("details",w.toString())); - listener.getLogger().println(); - } + 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(); } + } - // 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; } /** @@ -603,8 +570,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; @@ -628,7 +597,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); @@ -649,18 +618,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 @@ -715,6 +692,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; } @@ -751,19 +732,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. */ @@ -813,7 +802,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; @@ -882,6 +871,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. */ @@ -918,7 +913,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); @@ -951,7 +946,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(); } @@ -990,7 +994,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. @@ -1033,17 +1037,25 @@ public abstract class AbstractBuild

,R extends Abs } /** - * Gets {@link AbstractTestResultAction} associated with this build if any. + * @deprecated Use {@link #getAction(Class)} on {@link AbstractTestResultAction}. */ - public AbstractTestResultAction getTestResultAction() { - return getAction(AbstractTestResultAction.class); + public Action getTestResultAction() { + try { + return getAction(Jenkins.getInstance().getPluginManager().uberClassLoader.loadClass("hudson.tasks.test.AbstractTestResultAction").asSubclass(Action.class)); + } catch (ClassNotFoundException x) { + return null; + } } /** - * Gets {@link AggregatedTestResultAction} associated with this build if any. + * @deprecated Use {@link #getAction(Class)} on {@link AggregatedTestResultAction}. */ - public AggregatedTestResultAction getAggregatedTestResultAction() { - return getAction(AggregatedTestResultAction.class); + public Action getAggregatedTestResultAction() { + try { + return getAction(Jenkins.getInstance().getPluginManager().uberClassLoader.loadClass("hudson.tasks.test.AggregatedTestResultAction").asSubclass(Action.class)); + } catch (ClassNotFoundException x) { + return null; + } } /** diff --git a/core/src/main/java/hudson/model/AbstractCIBase.java b/core/src/main/java/hudson/model/AbstractCIBase.java index 69328fa4c963a1eb2ac675021516f047b5c8f947..cf9cec9bf27df85901cfcfd41bf502aacd9cecf4 100644 --- a/core/src/main/java/hudson/model/AbstractCIBase.java +++ b/core/src/main/java/hudson/model/AbstractCIBase.java @@ -43,9 +43,9 @@ import javax.annotation.CheckForNull; import jenkins.model.Configuration; public abstract class AbstractCIBase extends Node implements ItemGroup, StaplerProxy, StaplerFallback, ViewGroup, AccessControlled, DescriptorByNameOwner { - + public static boolean LOG_STARTUP_PERFORMANCE = Configuration.getBooleanConfigParameter("logStartupPerformance", false); - + private static final Logger LOGGER = Logger.getLogger(AbstractCIBase.class.getName()); private final transient Object updateComputerLock = new Object(); @@ -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()); + final Set old = new HashSet(computers.values()); Set used = new HashSet(); updateComputer(this, byName, used, automaticSlaveLaunch); 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 8a19a1d169c89d448a033bcfc7471591bffdc8cf..3524444feaf3fe4c837502244dd049509a48f14f 100644 --- a/core/src/main/java/hudson/model/AbstractItem.java +++ b/core/src/main/java/hudson/model/AbstractItem.java @@ -40,9 +40,11 @@ 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.DirectlyModifiableTopLevelItemGroup; import jenkins.model.Jenkins; +import jenkins.security.NotReallyRoleSensitiveCallable; +import org.acegisecurity.Authentication; import org.apache.tools.ant.taskdefs.Copy; import org.apache.tools.ant.types.FileSet; import org.kohsuke.stapler.WebMethod; @@ -52,6 +54,10 @@ 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; @@ -71,6 +77,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}. @@ -81,6 +88,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. */ @@ -202,7 +212,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet * Not all the Items need to support this operation, but if you decide to do so, * you can use this method. */ - protected void renameTo(String newName) throws IOException { + protected void renameTo(final String newName) throws IOException { // always synchronize from bigger objects first final ItemGroup parent = getParent(); synchronized (parent) { @@ -215,15 +225,31 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet if (this.name.equals(newName)) return; - Item existing = parent.getItem(newName); - if (existing != null && existing!=this) - // the look up is case insensitive, so we need "existing!=this" - // to allow people to rename "Foo" to "foo", for example. - // see http://www.nabble.com/error-on-renaming-project-tt18061629.html - throw new IllegalArgumentException("Job " + newName - + " already exists"); + // the test to see if the project already exists or not needs to be done in escalated privilege + // to avoid overwriting + ACL.impersonate(ACL.SYSTEM,new NotReallyRoleSensitiveCallable() { + final Authentication user = Jenkins.getAuthentication(); + @Override + public Void call() throws IOException { + Item existing = parent.getItem(newName); + if (existing != null && existing!=AbstractItem.this) { + if (existing.getACL().hasPermission(user,Item.DISCOVER)) + // the look up is case insensitive, so we need "existing!=this" + // to allow people to rename "Foo" to "foo", for example. + // see http://www.nabble.com/error-on-renaming-project-tt18061629.html + throw new IllegalArgumentException("Job " + newName + " already exists"); + else { + // can't think of any real way to hide this, but at least the error message could be vague. + throw new IOException("Unable to rename to " + newName); + } + } + return null; + } + }); + String oldName = this.name; + String oldFullName = getFullName(); File oldRoot = this.getRootDir(); doSetName(newName); @@ -292,24 +318,29 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet doSetName(oldName); } - callOnRenamed(newName, parent, oldName); + try { + parent.onRenamed(this, oldName, newName); + } catch (AbstractMethodError _) { + // ignore + } - for (ItemListener l : ItemListener.all()) - l.onRenamed(this, oldName, newName); + ItemListener.fireLocationChange(this, oldFullName); } } } + /** - * A pointless function to work around what appears to be a HotSpot problem. See JENKINS-5756 and bug 6933067 - * on BugParade for more details. + * Notify this item it's been moved to another location, replaced by newItem (might be the same object, but not guaranteed). + * This method is executed after the item root directory has been moved to it's new location. + *

+ * Derived classes can override this method to add some specific behavior on move, but have to call parent method + * so the item is actually setup within it's new parent. + * + * @see hudson.model.Items#move(AbstractItem, jenkins.model.DirectlyModifiableTopLevelItemGroup) */ - private void callOnRenamed(String newName, ItemGroup parent, String oldName) throws IOException { - try { - parent.onRenamed(this, oldName, newName); - } catch (AbstractMethodError _) { - // ignore - } + public void movedTo(DirectlyModifiableTopLevelItemGroup destination, AbstractItem newItem, File destDir) throws IOException { + newItem.onLoad(destination, name); } /** @@ -367,7 +398,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; @@ -392,16 +423,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() { @@ -487,8 +543,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 { @@ -507,27 +578,15 @@ 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 + getParent().onDeleted(AbstractItem.this); Jenkins.getInstance().rebuildDependencyGraphAsync(); } - /** - * A pointless function to work around what appears to be a HotSpot problem. See JENKINS-5756 and bug 6933067 - * on BugParade for more details. - */ - private void invokeOnDeleted() throws IOException { - getParent().onDeleted(this); - } - /** * Does the real job of deleting the item. */ @@ -586,17 +645,23 @@ 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); + Object o = new XmlFile(Items.XSTREAM, out.getTemporaryFile()).unmarshal(this); + if (o!=this) { + // ensure that we've got the same job type. extending this code to support updating + // to different job type requires destroying & creating a new job type + throw new IOException("Expecting "+this.getClass()+" but got "+o.getClass()+" instead"); } + + Items.whileUpdatingByXml(new NotReallyRoleSensitiveCallable() { + @Override public Void call() throws IOException { + onLoad(getParent(), getRootDir().getName()); + return null; + } + }); Jenkins.getInstance().rebuildDependencyGraphAsync(); // if everything went well, commit this new version @@ -607,6 +672,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 NotReallyRoleSensitiveCallable() { + @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() @@ -619,8 +712,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) + ']'; } /** @@ -629,6 +722,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())); @@ -639,4 +733,5 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet * Replaceable pronoun of that points to a job. Defaults to "Job"/"Project" depending on the context. */ public static final Message PRONOUN = new Message(); + } diff --git a/core/src/main/java/hudson/model/AbstractModelObject.java b/core/src/main/java/hudson/model/AbstractModelObject.java index c104a3179c1ac7e087d75a240e888c1802d1947a..301d6d09b3765a9b1ca449c1536d4706aacbe661 100644 --- a/core/src/main/java/hudson/model/AbstractModelObject.java +++ b/core/src/main/java/hudson/model/AbstractModelObject.java @@ -47,6 +47,7 @@ public abstract class AbstractModelObject implements SearchableModelObject { * Displays the error in a page. */ protected final void sendError(Exception e, StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { + req.setAttribute("exception", e); sendError(e.getMessage(),req,rsp); } diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index 32584041b4420ccac9f6285f0edf0231d68b4df7..fe49923fac53350c517b0a4fe47ae7a79b0f3cb8 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -1,22 +1,22 @@ /* * The MIT License - * + * * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Brian Westrich, Erik Ramfelt, Ertan Deniz, Jean-Baptiste Quenot, * Luca Domenico Milanesio, R. Tyler Ballance, Stephen Connolly, Tom Huybrechts, * id:cactusman, Yahoo! Inc., Andrew Bayer, Manufacture Francaise des Pneumatiques * Michelin, Romain Seguy - * + * * 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 @@ -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.ExtensionList; +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.PermalinkProjectAction.Permalink; +import hudson.model.Node.Mode; 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; @@ -82,66 +83,56 @@ import hudson.util.AlternativeUiTextProvider.Message; import hudson.util.DescribableList; import hudson.util.FormValidation; import hudson.util.TimeUnit2; -import hudson.widgets.BuildHistoryWidget; 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.ParameterizedJobMixIn; import jenkins.model.Uptime; -import jenkins.model.lazy.AbstractLazyLoadRunMap.Direction; +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.DoNotUse; 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. * @@ -151,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, LazyBuildMixIn.LazyLoadingJob, ParameterizedJobMixIn.ParameterizedJob { /** * {@link SCM} associated with the project. @@ -170,21 +161,22 @@ 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. */ private volatile Integer quietPeriod = null; - + /** * The retry count. Null to delegate to the system default. */ @@ -268,9 +260,11 @@ public abstract class AbstractProject

,R extends A * @since 1.410 */ private String customWorkspace; - + 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 @@ -279,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(); @@ -288,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(); } @@ -296,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. @@ -334,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) { @@ -354,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 && !jdk.equals(JDK.DEFAULT_NAME)) { + listener.getLogger().println("No JDK named ‘" + jdk + "’ found"); } return env; @@ -397,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; @@ -472,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()); } /** @@ -517,11 +510,11 @@ public abstract class AbstractProject

,R extends A return b != null ? b.getWorkspace() : null; } - + /** * Various deprecated methods in this class all need the 'current' build. This method returns * the build suitable for that purpose. - * + * * @return An AbstractBuild for deprecated methods to use. */ private AbstractBuild getBuildForDeprecatedMethods() { @@ -551,10 +544,10 @@ public abstract class AbstractProject

,R extends A * null if there's no available workspace. * @since 1.319 */ - public final FilePath getSomeWorkspace() { + public final @CheckForNull FilePath getSomeWorkspace() { R b = getSomeBuildWithWorkspace(); if (b!=null) return b.getWorkspace(); - for (WorkspaceBrowser browser : Jenkins.getInstance().getExtensionList(WorkspaceBrowser.class)) { + for (WorkspaceBrowser browser : ExtensionList.lookup(WorkspaceBrowser.class)) { FilePath f = browser.getWorkspace(this); if (f != null) return f; } @@ -574,7 +567,7 @@ public abstract class AbstractProject

,R extends A } return null; } - + private R getSomeBuildWithExistingWorkspace() throws IOException, InterruptedException { int cnt=0; for (R b = getLastBuild(); cnt<5 && b!=null; b=b.getPreviousBuild()) { @@ -637,13 +630,13 @@ public abstract class AbstractProject

,R extends A } /** - * Sets the custom quiet period of this project, or revert to the global default if null is given. + * Sets the custom quiet period of this project, or revert to the global default if null is given. */ public void setQuietPeriod(Integer seconds) throws IOException { this.quietPeriod = seconds; save(); } - + public boolean hasCustomScmCheckoutRetryCount(){ return scmCheckoutRetryCount != null; } @@ -682,7 +675,7 @@ public abstract class AbstractProject

,R extends A public boolean isDisabled() { return disabled; } - + /** * Validates the retry count Regex */ @@ -692,19 +685,24 @@ public abstract class AbstractProject

,R extends A return FormValidation.ok(); if (!value.matches("[0-9]*")) { return FormValidation.error("Invalid retry count"); - } + } return FormValidation.ok(); } /** * Marks the build as disabled. + * The method will ignore the disable command if {@link #supportsMakeDisabled()} + * returns false. The enable command will be executed in any case. + * @param b true - disable, false - enable + * @since 1.585 Do not disable projects if {@link #supportsMakeDisabled()} returns false */ public void makeDisabled(boolean b) throws IOException { if(disabled==b) return; // noop + if (b && !supportsMakeDisabled()) return; // do nothing if the disabling is unsupported this.disabled = b; if(b) Jenkins.getInstance().getQueue().cancel(this); - + save(); ItemListener.fireOnUpdated(this); } @@ -731,7 +729,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(); } @@ -774,13 +772,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 @@ -789,13 +781,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(); @@ -803,82 +788,22 @@ 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(); } - + /** * @deprecated * Use {@link #scheduleBuild(int, Cause)}. Since 1.283 */ public boolean scheduleBuild(int quietPeriod) { - return scheduleBuild(quietPeriod, new LegacyCodeCause()); + return getParameterizedJobMixIn().scheduleBuild(quietPeriod); } - + /** * Schedules a build of this project. * @@ -887,11 +812,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); } /** @@ -933,44 +858,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()])); } /** @@ -986,7 +878,7 @@ public abstract class AbstractProject

,R extends A public QueueTaskFuture scheduleBuild2(int quietPeriod) { return scheduleBuild2(quietPeriod, new LegacyCodeCause()); } - + /** * Schedules a build of this project, and returns a {@link Future} object * to wait for the completion of the build. @@ -1041,18 +933,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); } /** @@ -1062,7 +948,7 @@ public abstract class AbstractProject

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

,R extends A */ @Override public R getBuildByNumber(int n) { - return builds.getByNumber(n); + return buildMixIn.getBuildByNumber(n); } /** @@ -1082,84 +968,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); } /** @@ -1172,6 +1018,7 @@ public abstract class AbstractProject

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

,R extends A } public Object getSameNodeConstraint() { - return this; // in this way, any member that wants to run with the main guy can nominate the project itself + return this; // in this way, any member that wants to run with the main guy can nominate the project itself } public final Task getOwnerTask() { @@ -1211,6 +1058,12 @@ public abstract class AbstractProject

,R extends A return ACL.SYSTEM; } + @Nonnull + @Override + public Authentication getDefaultAuthentication(Queue.Item item) { + return getDefaultAuthentication(); + } + /** * {@inheritDoc} * @@ -1248,7 +1101,7 @@ public abstract class AbstractProject

,R extends A return Messages.AbstractProject_BuildInProgress(lbn, eta); } } - + /** * Because the downstream build is in progress, and we are configured to wait for that. */ @@ -1342,17 +1195,17 @@ public abstract class AbstractProject

,R extends A return r; } - public R createExecutable() throws IOException { + public @CheckForNull R createExecutable() throws IOException { if(isDisabled()) return null; return newBuild(); } public void checkAbortPermission() { - checkPermission(AbstractProject.ABORT); + checkPermission(CANCEL); } public boolean hasAbortPermission() { - return hasPermission(AbstractProject.ABORT); + return hasPermission(CANCEL); } /** @@ -1408,7 +1261,7 @@ public abstract class AbstractProject

,R extends A new DiskSpaceMonitor().markNodeOfflineIfDiskspaceIsTooLow(build.getBuiltOn().toComputer()); throw e; } - + boolean r = scm.checkout(build, launcher, workspace, listener, changelogFile); if (r) { // Only calcRevisionsFromBuild if checkout was successful. Note that modern SCM implementations @@ -1425,7 +1278,7 @@ public abstract class AbstractProject

,R extends A SCMRevisionState baseline = build.getAction(SCMRevisionState.class); if (baseline==null) { try { - baseline = getScm()._calcRevisionsFromBuild(build, launcher, listener); + baseline = getScm().calcRevisionsFromBuild(build, launcher, listener); } catch (AbstractMethodError e) { baseline = SCMRevisionState.NONE; // pre-1.345 SCM implementations, which doesn't use the baseline in polling } @@ -1527,7 +1380,7 @@ public abstract class AbstractProject

,R extends A WorkspaceOfflineReason workspaceOfflineReason = workspaceOffline( b ); if ( workspaceOfflineReason != null ) { // workspace offline - for (WorkspaceBrowser browser : Jenkins.getInstance().getExtensionList(WorkspaceBrowser.class)) { + for (WorkspaceBrowser browser : ExtensionList.lookup(WorkspaceBrowser.class)) { ws = browser.getWorkspace(this); if (ws != null) { return pollWithWorkspace(listener, scm, b, ws, browser.getWorkspaceList()); @@ -1537,16 +1390,23 @@ public abstract class AbstractProject

,R extends A // 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) { + 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 @@ -1571,12 +1431,11 @@ public abstract class AbstractProject

,R extends A } 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); @@ -1585,7 +1444,7 @@ public abstract class AbstractProject

,R extends A } } - private PollingResult pollWithWorkspace(TaskListener listener, SCM scm, R lb, FilePath ws, WorkspaceList l) throws InterruptedException, IOException { + private PollingResult pollWithWorkspace(TaskListener listener, SCM scm, R lb, @Nonnull FilePath ws, WorkspaceList l) throws InterruptedException, IOException { // if doing non-concurrent build, acquire a workspace in a way that causes builds to block for this workspace. // this prevents multiple workspaces of the same job --- the behavior of Hudson < 1.319. // @@ -1611,20 +1470,64 @@ 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; } - + Node builtOn = build.getBuiltOn(); if (builtOn == null) { // node built-on doesn't exist anymore return WorkspaceOfflineReason.builton_node_gone; } - + if (builtOn.toComputer() == null) { // node still exists, but has 0 executors - o.s.l.t. return WorkspaceOfflineReason.builton_node_no_executors; } @@ -1690,7 +1593,7 @@ public abstract class AbstractProject

,R extends A } @SuppressWarnings("unchecked") - public Map> getTriggers() { + @Override public Map> getTriggers() { return triggers().toMap(); } @@ -1732,7 +1635,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() { @@ -1742,10 +1645,10 @@ public abstract class AbstractProject

,R extends A if (buildTrigger != null) if (buildTrigger.getChildProjects(ap).contains(this)) result.add(ap); - } + } return result; - } - + } + /** * Gets all the upstream projects including transitive upstream projects. * @@ -1804,25 +1707,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(); } // @@ -1834,31 +1736,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)} */ @@ -1867,21 +1745,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. * @@ -1907,15 +1770,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)} */ @@ -1928,7 +1783,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("."); } @@ -1938,60 +1793,39 @@ 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); JSONObject json = req.getSubmittedForm(); - makeDisabled(req.getParameter("disable")!=null); + makeDisabled(json.optBoolean("disable")); + + jdk = json.optString("jdk", null); - jdk = req.getParameter("jdk"); - if(req.getParameter("hasCustomQuietPeriod")!=null) { - quietPeriod = Integer.parseInt(req.getParameter("quiet_period")); + if(json.optBoolean("hasCustomQuietPeriod", json.has("quiet_period"))) { + quietPeriod = json.optInt("quiet_period"); } else { quietPeriod = null; } - if(req.getParameter("hasCustomScmCheckoutRetryCount")!=null) { - scmCheckoutRetryCount = Integer.parseInt(req.getParameter("scmCheckoutRetryCount")); + + if(json.optBoolean("hasCustomScmCheckoutRetryCount", json.has("scmCheckoutRetryCount"))) { + scmCheckoutRetryCount = json.optInt("scmCheckoutRetryCount"); } else { scmCheckoutRetryCount = null; } - blockBuildWhenDownstreamBuilding = req.getParameter("blockBuildWhenDownstreamBuilding")!=null; - blockBuildWhenUpstreamBuilding = req.getParameter("blockBuildWhenUpstreamBuilding")!=null; - if(req.hasParameter("customWorkspace")) { + blockBuildWhenDownstreamBuilding = json.optBoolean("blockBuildWhenDownstreamBuilding"); + blockBuildWhenUpstreamBuilding = json.optBoolean("blockBuildWhenUpstreamBuilding"); + + if(req.hasParameter("customWorkspace.directory")) { + // Workaround for JENKINS-25221 while plugins are being updated. + LOGGER.log(Level.WARNING, "label assignment is using legacy 'customWorkspace.directory'"); customWorkspace = Util.fixEmptyAndTrim(req.getParameter("customWorkspace.directory")); + } else if(json.optBoolean("hasCustomWorkspace", json.has("customWorkspace"))) { + customWorkspace = Util.fixEmptyAndTrim(json.optString("customWorkspace")); } else { customWorkspace = null; } @@ -2002,15 +1836,21 @@ public abstract class AbstractProject

,R extends A else scmCheckoutStrategy = null; - - if(req.getParameter("hasSlaveAffinity")!=null) { + if(json.optBoolean("hasSlaveAffinity", json.has("label"))) { + assignedNode = Util.fixEmptyAndTrim(json.optString("label")); + } else if(req.hasParameter("_.assignedLabelString")) { + // Workaround for JENKINS-25372 while plugin is being updated. + // Keep this condition second for JENKINS-25533 + LOGGER.log(Level.WARNING, "label assignment is using legacy '_.assignedLabelString'"); assignedNode = Util.fixEmptyAndTrim(req.getParameter("_.assignedLabelString")); - } else { + } else { assignedNode = null; } canRoam = assignedNode==null; - concurrentBuild = req.getSubmittedForm().has("concurrentBuild"); + keepDependencies = json.has("keepDependencies"); + + concurrentBuild = json.optBoolean("concurrentBuild"); authToken = BuildAuthorizationToken.create(req); @@ -2021,13 +1861,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,17 +1886,6 @@ public abstract class AbstractProject

,R extends A return r; } - public ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { - // not sure what would be really useful here. This needs more thoughts. - // for the time being, I'm starting with permalinks - ContextMenu menu = new ContextMenu(); - for (Permalink p : getPermalinks()) { - if (p.resolve(this)!=null) - menu.add(p.getId(),p.getDisplayName()); - } - return menu; - } - /** * Serves the workspace files. */ @@ -2079,7 +1901,14 @@ public abstract class AbstractProject

,R extends A req.getView(this,"noWorkspace.jelly").forward(req,rsp); return null; } else { - return new DirectoryBrowserSupport(this, ws, getDisplayName()+" workspace", "folder.png", true); + Computer c = ws.toComputer(); + String title; + if (c == null) { + title = Messages.AbstractProject_WorkspaceTitle(getDisplayName()); + } else { + title = Messages.AbstractProject_WorkspaceTitleOnComputer(getDisplayName(), c.getDisplayName()); + } + return new DirectoryBrowserSupport(this, ws, title, "folder.png", true); } } @@ -2118,6 +1947,7 @@ public abstract class AbstractProject

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

,R extends A *

* The default implementation returns true for everything. * - * @see BuildStepDescriptor#isApplicable(Class) - * @see BuildWrapperDescriptor#isApplicable(AbstractProject) + * @see BuildStepDescriptor#isApplicable(Class) + * @see BuildWrapperDescriptor#isApplicable(AbstractProject) * @see TriggerDescriptor#isApplicable(Item) */ @Override @@ -2209,7 +2039,27 @@ public abstract class AbstractProject

,R extends A return true; } - public FormValidation doCheckAssignedLabelString(@QueryParameter String value) { + @Restricted(DoNotUse.class) + public FormValidation doCheckAssignedLabelString(@AncestorInPath AbstractProject project, + @QueryParameter String value) { + // Provide a legacy interface in case plugins are not going through p:config-assignedLabel + // see: JENKINS-25372 + LOGGER.log(Level.WARNING, "checking label via legacy '_.assignedLabelString'"); + return doCheckLabel(project, value); + } + + public FormValidation doCheckLabel(@AncestorInPath AbstractProject project, + @QueryParameter String value) { + return validateLabelExpression(value, project); + } + + /** + * Validate label expression string. + * + * @param project May be specified to perform project specific validation. + * @since 1.590 + */ + public static @Nonnull FormValidation validateLabelExpression(String value, @CheckForNull AbstractProject project) { if (Util.fixEmpty(value)==null) return FormValidation.ok(); // nothing typed yet try { @@ -2218,7 +2068,11 @@ 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(); + if (j == null) { + return FormValidation.ok(); // ? + } + Label l = j.getLabel(value); if (l.isEmpty()) { for (LabelAtom a : l.listAtoms()) { if (a.isEmpty()) { @@ -2228,16 +2082,27 @@ 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){ + public FormValidation doCheckCustomWorkspace(@QueryParameter String customWorkspace){ if(Util.fixEmptyAndTrim(customWorkspace)==null) return FormValidation.error(Messages.AbstractProject_CustomWorkspaceEmpty()); else return FormValidation.ok(); } - + public AutoCompletionCandidates doAutoCompleteUpstreamProjects(@QueryParameter String value) { AutoCompletionCandidates candidates = new AutoCompletionCandidates(); List jobs = Jenkins.getInstance().getItems(Job.class); @@ -2251,7 +2116,15 @@ public abstract class AbstractProject

,R extends A return candidates; } + @Restricted(DoNotUse.class) public AutoCompletionCandidates doAutoCompleteAssignedLabelString(@QueryParameter String value) { + // Provide a legacy interface in case plugins are not going through p:config-assignedLabel + // see: JENKINS-25372 + LOGGER.log(Level.WARNING, "autocompleting label via legacy '_.assignedLabelString'"); + return doAutoCompleteLabel(value); + } + + public AutoCompletionCandidates doAutoCompleteLabel(@QueryParameter String value) { AutoCompletionCandidates c = new AutoCompletionCandidates(); Set

,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()); } /** @@ -2326,7 +2199,7 @@ public abstract class AbstractProject

,R extends A * @since 1.419 * @see Items#findNearest */ - public static AbstractProject findNearest(String name, ItemGroup context) { + public static @CheckForNull AbstractProject findNearest(String name, ItemGroup context) { return Items.findNearest(AbstractProject.class, name, context); } @@ -2339,7 +2212,7 @@ public abstract class AbstractProject

,R extends A private static final Logger LOGGER = Logger.getLogger(AbstractProject.class.getName()); /** - * Permission to abort a build + * @deprecated Just use {@link #CANCEL}. */ public static final Permission ABORT = CANCEL; @@ -2376,7 +2249,7 @@ public abstract class AbstractProject

,R extends A * *

* If this path is relative, it's resolved against {@link Node#getRootPath()} on the node where this workspace - * is prepared. + * is prepared. * * @since 1.410 */ @@ -2384,5 +2257,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..f73bd34032f670fe3b90db60797184fcd96b0e3a 100644 --- a/core/src/main/java/hudson/model/Action.java +++ b/core/src/main/java/hudson/model/Action.java @@ -24,7 +24,6 @@ package hudson.model; import hudson.Functions; -import hudson.tasks.test.TestResultProjectAction; /** * Object that contributes additional information, behaviors, and UIs to {@link ModelObject} @@ -46,7 +45,7 @@ import hudson.tasks.test.TestResultProjectAction; * it will be displayed as a floating box on the top page of * the target {@link ModelObject}. (For example, this is how * the JUnit test result trend shows up in the project top page. - * See {@link TestResultProjectAction}. + * See {@code TestResultProjectAction}.) * *

* On the target {@link ModelObject} page, actions are rendered as an item in the side panel @@ -56,8 +55,8 @@ import hudson.tasks.test.TestResultProjectAction; * Jenkins show the option to wipe out the workspace inside the workspace link: * *

- * <l:task icon="images/24x24/folder.gif"  href="${url}/ws/" title="${%Workspace}">
- *   <l:task icon="images/24x24/folder-delete.gif"  href="${url}/wipeOutWorkspace" title="${%Wipe Out Workspace}" />
+ * <l:task icon="icon-folder icon-md"  href="${url}/ws/" title="${%Workspace}">
+ *   <l:task icon="icon-delete icon-md"  href="${url}/wipeOutWorkspace" title="${%Wipe Out Workspace}" />
  * </l:task>
  * 
* @@ -104,7 +103,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..ba19d39257960e4fdf77254ad94e9ebd9e3d0c70 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.ExtensionList; +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.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,27 @@ 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()); + for (TransientActionFactory taf : ExtensionList.lookup(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 +109,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 +155,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 +174,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/AdministrativeMonitor.java b/core/src/main/java/hudson/model/AdministrativeMonitor.java index 93febdbd75935bfc6778ac098e606dff78c93ea7..a8e555409214ac44570ffbd1384d8c53b887630d 100644 --- a/core/src/main/java/hudson/model/AdministrativeMonitor.java +++ b/core/src/main/java/hudson/model/AdministrativeMonitor.java @@ -153,6 +153,6 @@ public abstract class AdministrativeMonitor extends AbstractModelObject implemen * All registered {@link AdministrativeMonitor} instances. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(AdministrativeMonitor.class); + return ExtensionList.lookup(AdministrativeMonitor.class); } } diff --git a/core/src/main/java/hudson/model/AllView.java b/core/src/main/java/hudson/model/AllView.java index 0bc73d3c58bb53b69d8c7f5d429b980386fd55be..6cf8f2448d7c10734a06b54dbba4f3cf407ec92b 100644 --- a/core/src/main/java/hudson/model/AllView.java +++ b/core/src/main/java/hudson/model/AllView.java @@ -34,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. @@ -62,6 +63,7 @@ public class AllView extends View { return true; } + @RequirePOST @Override public Item doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { @@ -81,11 +83,6 @@ public class AllView 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/AperiodicWork.java b/core/src/main/java/hudson/model/AperiodicWork.java index 512db0efc0e0f4cf3af11f003790c2c03c6f6171..56b0804da1ec2f949193a67c93d99e23db88ea70 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,19 +88,24 @@ 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(); /** * Returns all the registered {@link AperiodicWork}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(AperiodicWork.class); + return ExtensionList.lookup(AperiodicWork.class); } private static final Random RANDOM = new Random(); diff --git a/core/src/main/java/hudson/model/Api.java b/core/src/main/java/hudson/model/Api.java index 652fcf9e856121c986ac74767c80b917bf1e6c0b..c749f46d133336ef38979dae3a6a0c275a0184a8 100644 --- a/core/src/main/java/hudson/model/Api.java +++ b/core/src/main/java/hudson/model/Api.java @@ -23,7 +23,7 @@ */ package hudson.model; -import hudson.util.IOException2; +import hudson.ExtensionList; import jenkins.model.Jenkins; import jenkins.security.SecureRequester; @@ -152,19 +152,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 (permit(req)) { - 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; implement jenkins.security.SecureRequester"); - } + 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; } @@ -176,6 +181,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. */ @@ -208,7 +217,7 @@ public class Api extends AbstractModelObject { } private boolean permit(StaplerRequest req) { - for (SecureRequester r : Jenkins.getInstance().getExtensionList(SecureRequester.class)) { + for (SecureRequester r : ExtensionList.lookup(SecureRequester.class)) { if (r.permit(req, bean)) { return true; } 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 1554d1ebe19ef03d06dac5baedacfbcfba724d37..450b216cfb83a45c32bc798bc5051a6d0a205f00 100644 --- a/core/src/main/java/hudson/model/BallColor.java +++ b/core/src/main/java/hudson/model/BallColor.java @@ -25,6 +25,7 @@ package hudson.model; import hudson.util.ColorPalette; import jenkins.model.Jenkins; +import org.jenkins.ui.icon.Icon; import org.jvnet.localizer.LocaleProvider; import org.jvnet.localizer.Localizable; import org.kohsuke.stapler.Stapler; @@ -72,10 +73,14 @@ public enum BallColor implements StatusIcon { ; private final Localizable description; + private final String iconName; + private final String iconClassName; private final String image; private final Color baseColor; BallColor(String image, Localizable description, Color baseColor) { + this.iconName = Icon.toNormalizedIconName(image); + this.iconClassName = Icon.toNormalizedIconNameClass(image); this.baseColor = baseColor; // name() is not usable in the constructor, so I have to repeat the name twice // in the constants definition. @@ -83,6 +88,22 @@ public enum BallColor implements StatusIcon { this.description = description; } + /** + * Get the status ball icon name. + * @return The status ball icon name. + */ + public String getIconName() { + return iconName; + } + + /** + * Get the status ball icon class spec name. + * @return The status ball icon class spec name. + */ + public String getIconClassName() { + return iconClassName; + } + /** * String like "red.png" that represents the file name of the image. */ @@ -112,7 +133,7 @@ public enum BallColor implements StatusIcon { * Returns the {@link #getBaseColor()} in the "#RRGGBB" format. */ public String getHtmlBaseColor() { - return String.format("#%06X",baseColor.getRGB()&0xFFFFFF); + return String.format("#%06X", baseColor.getRGB() & 0xFFFFFF); } /** 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 1fba190925e0278de84ba087fa94bc3ebe12e548..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 diff --git a/core/src/main/java/hudson/model/BuildStepListener.java b/core/src/main/java/hudson/model/BuildStepListener.java index 03f36a0f201a1a0adc20931b429a30f7ed9a4e6a..a37cb8f67abfa6f37622d042ff09038207f4a6dd 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() { - return Jenkins.getInstance().getExtensionList(BuildStepListener.class); + // TODO should have a null-safe version when Jenkins.getInstance() is null; would require changes in ExtensionList + return ExtensionList.lookup(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/BuildVariableContributor.java b/core/src/main/java/hudson/model/BuildVariableContributor.java index 23b6632644882a3592d1e8eabfd1a97baf804da3..15baad759088c83ce5deb3cde44e56e7b4103e68 100644 --- a/core/src/main/java/hudson/model/BuildVariableContributor.java +++ b/core/src/main/java/hudson/model/BuildVariableContributor.java @@ -70,6 +70,6 @@ public abstract class BuildVariableContributor implements ExtensionPoint { * Returns all the registered {@link BuildVariableContributor}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(BuildVariableContributor.class); + return ExtensionList.lookup(BuildVariableContributor.class); } } diff --git a/core/src/main/java/hudson/model/Cause.java b/core/src/main/java/hudson/model/Cause.java index 8283cfa68c515b037e6fa1dcd02526a0e51f66f7..37dbcee4ca753fdb5fa11f88eac7626fe77d5e9f 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,21 +70,47 @@ 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(AbstractBuild build) {} + 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.540 + * @since 1.568 */ - public void onLoad(@Nonnull AbstractBuild build) {} + public void onLoad(@Nonnull Run build) { + if (build instanceof AbstractBuild) { + onLoad((AbstractBuild) build); + } + } + + void onLoad(@Nonnull Job job, int buildNumber) { + Run build = job.getBuildByNumber(buildNumber); + if (build != null) { + onLoad(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. @@ -109,7 +137,7 @@ public abstract class Cause { } /** - * A build is triggered by the completion of another build (AKA upstream build.) + * A build is triggered by another build (AKA upstream build.) */ public static class UpstreamCause extends Cause { @@ -156,6 +184,20 @@ public abstract class Cause { this.upstreamCauses = upstreamCauses; } + @Override + public void onLoad(@Nonnull Job _job, int _buildNumber) { + Item i = Jenkins.getInstance().getItemByFullName(this.upstreamProject); + if (i == null || !(i instanceof Job)) { + // cannot initialize upstream causes + return; + } + + Job j = (Job)i; + for (Cause c : this.upstreamCauses) { + c.onLoad(j, upstreamBuild); + } + } + /** * @since 1.515 */ @@ -308,6 +350,7 @@ public abstract class Cause { @Override public String toString() { return "JENKINS-14814"; } + @Override public void onLoad(@Nonnull Job _job, int _buildNumber) {} } } @@ -326,8 +369,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 @@ -368,13 +410,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 @@ -385,6 +421,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()))); } @@ -412,10 +449,23 @@ 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); + } + + @Exported(visibility = 3) + public String getAddr() { + return addr; + } + + @Exported(visibility = 3) + public String getNote() { + return note; } @Override diff --git a/core/src/main/java/hudson/model/CauseAction.java b/core/src/main/java/hudson/model/CauseAction.java index e823afdfed798c82bd959a402d1007f8f35e43c1..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; } @@ -117,10 +119,9 @@ public class CauseAction implements FoldableAction, RunAction2 { } @Override public void onLoad(Run owner) { - if (owner instanceof AbstractBuild) { // cf. onAttached - AbstractBuild b = (AbstractBuild) owner; - for (Cause c : causes) { - c.onLoad(b); + for (Cause c : causes) { + if (c != null) { + c.onLoad(owner); } } } @@ -129,10 +130,9 @@ public class CauseAction implements FoldableAction, RunAction2 { * 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); } } } @@ -144,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/CheckPoint.java b/core/src/main/java/hudson/model/CheckPoint.java index 7a5e330af6ad93912c9d47845c2fc9de2a08ea2a..934fe7ddb500478be62c76a3db922d8ba0079bdd 100644 --- a/core/src/main/java/hudson/model/CheckPoint.java +++ b/core/src/main/java/hudson/model/CheckPoint.java @@ -26,7 +26,6 @@ package hudson.model; import hudson.tasks.BuildStep; import hudson.tasks.Recorder; import hudson.tasks.Builder; -import hudson.tasks.junit.JUnitResultArchiver; import hudson.scm.SCM; import javax.annotation.Nonnull; @@ -53,9 +52,11 @@ import javax.annotation.Nonnull; * This class defines a few well-known check point instances. plugins can define * their additional check points for their own use. * + *

Note that not all job types support checkpoints. + * *

Example

*

- * {@link JUnitResultArchiver} provides a good example of how a {@link Recorder} can + * {@code JUnitResultArchiver} provides a good example of how a {@link Recorder} can * depend on its earlier result. * * @author Kohsuke Kawaguchi @@ -125,9 +126,9 @@ public final class CheckPoint { * *

    *
  1. Build #1, #2, and #3 happens around the same time - *
  2. Build #3 waits for check point {@link JUnitResultArchiver} + *
  3. Build #3 waits for check point {@code JUnitResultArchiver} *
  4. Build #2 aborts before getting to that check point - *
  5. Build #1 finally checks in {@link JUnitResultArchiver} + *
  6. Build #1 finally checks in {@code JUnitResultArchiver} *
* *

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 4da0c2998510ac9ffc96c09953a2ad45daf4ff0c..c13137d62a7b2f833497b302cb66960d74c17936 --- a/core/src/main/java/hudson/model/ChoiceParameterDefinition.java +++ b/core/src/main/java/hudson/model/ChoiceParameterDefinition.java @@ -72,7 +72,7 @@ public class ChoiceParameterDefinition extends SimpleParameterDefinition { private StringParameterValue checkValue(StringParameterValue value) { if (!choices.contains(value.value)) - throw new IllegalArgumentException("Illegal choice: " + value.value); + throw new IllegalArgumentException("Illegal choice for parameter " + getName() + ": " + value.value); return value; } diff --git a/core/src/main/java/hudson/model/Computer.java b/core/src/main/java/hudson/model/Computer.java index e919e91838fcd26ba1eabf6b815416a3cc2aca0b..2d23453fad5293ef1a35ab79dc98938cb3ab20c8 100644 --- a/core/src/main/java/hudson/model/Computer.java +++ b/core/src/main/java/hudson/model/Computer.java @@ -24,6 +24,8 @@ */ package hudson.model; +import edu.umd.cs.findbugs.annotations.OverrideMustInvoke; +import edu.umd.cs.findbugs.annotations.When; import hudson.EnvVars; import hudson.Launcher.ProcStarter; import hudson.Util; @@ -38,7 +40,6 @@ import hudson.model.queue.WorkUnit; import hudson.node_monitors.NodeMonitor; import hudson.remoting.Channel; import hudson.remoting.VirtualChannel; -import hudson.remoting.Callable; import hudson.security.ACL; import hudson.security.AccessControlled; import hudson.security.Permission; @@ -51,13 +52,17 @@ 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 jenkins.security.MasterToSlaveCallable; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.stapler.StaplerRequest; @@ -85,8 +90,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 +100,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.*; @@ -106,14 +110,14 @@ import static javax.servlet.http.HttpServletResponse.*; * *

* {@link Executor}s on one {@link Computer} are transparently interchangeable - * (that is the definition of {@link Computer}.) + * (that is the definition of {@link Computer}). * *

- * This object is related to {@link Node} but they have some significant difference. + * This object is related to {@link Node} but they have some significant differences. * {@link Computer} primarily works as a holder of {@link Executor}s, so * if a {@link Node} is configured (probably temporarily) with 0 executors, * you won't have a {@link Computer} object for it (except for the master node, - * which always get its {@link Computer} in case we have no static executors and + * which always gets its {@link Computer} in case we have no static executors and * we need to run a {@link FlyweightTask} - see JENKINS-7291 for more discussion.) * * Also, even if you remove a {@link Node}, it takes time for the corresponding @@ -122,8 +126,8 @@ import static javax.servlet.http.HttpServletResponse.*; * remains intact, while all the {@link Node} objects will go away. * *

- * This object also serves UI (since {@link Node} is an interface and can't have - * related side pages.) + * This object also serves UI (unlike {@link Node}), and can be used along with + * {@link TransientComputerActionFactory} to add {@link Action}s to {@link Computer}s. * * @author Kohsuke Kawaguchi */ @@ -131,7 +135,7 @@ import static javax.servlet.http.HttpServletResponse.*; public /*transient*/ abstract class Computer extends Actionable implements AccessControlled, ExecutorListener { private final CopyOnWriteArrayList executors = new CopyOnWriteArrayList(); - // TODO: + // TODO: private final CopyOnWriteArrayList oneOffExecutors = new CopyOnWriteArrayList(); private int numExecutors; @@ -140,7 +144,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces * Contains info about reason behind computer being offline. */ protected volatile OfflineCause offlineCause; - + private long connectTime = 0; /** @@ -176,10 +180,11 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces public List getComputerPanelBoxs(){ return ComputerPanelBox.all(this); } - + /** * 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 +194,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,12 +206,14 @@ 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() { + public File getLogFile() { File dir = new File(Jenkins.getInstance().getRootDir(),"logs/slaves/"+nodeName); - dir.mkdirs(); + if (!dir.exists() && !dir.mkdirs()) { + LOGGER.severe("Failed to create slave log directory " + dir.getAbsolutePath()); + } return new File(dir,"slave.log"); } @@ -334,7 +342,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces connectTime = System.currentTimeMillis(); return _connect(forceReconnect); } - + /** * Allows implementing-classes to provide an implementation for the connect method. * @@ -367,13 +375,13 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces /** * Gets the time (since epoch) when this computer connected. - * + * * @return The time in ms since epoch when this computer last connected. */ public final long getConnectTime() { return connectTime; } - + /** * Disconnect this computer. * @@ -452,8 +460,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 : ""; } /** @@ -464,14 +472,19 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces * is not yet gone. */ public @CheckForNull Node getNode() { - if(nodeName==null) - return Jenkins.getInstance(); - return Jenkins.getInstance().getNode(nodeName); + Jenkins j = Jenkins.getInstance(); + if (j == null) { + return null; + } + if (nodeName == null) { + return j; + } + return j.getNode(nodeName); } @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() { @@ -578,7 +591,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces * * @param cause * If the first argument is true, specify the reason why the node is being put - * offline. + * offline. */ public void setTemporarilyOffline(boolean temporarilyOffline, OfflineCause cause) { offlineCause = temporarilyOffline ? cause : null; @@ -605,6 +618,14 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces return "computer.png"; } + @Exported + public String getIconClassName() { + if(isOffline()) + return "icon-computer-x"; + else + return "icon-computer"; + } + public String getIconAltText() { if(isOffline()) return "[offline]"; @@ -613,7 +634,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces } @Exported - public String getDisplayName() { + @Override public @Nonnull String getDisplayName() { return nodeName; } @@ -622,7 +643,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces } public String getUrl() { - return "computer/" + Util.rawEncode(getDisplayName()) + "/"; + return "computer/" + Util.rawEncode(getName()) + "/"; } /** @@ -692,14 +713,20 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces private synchronized void setNumExecutors(int n) { this.numExecutors = n; - int diff = executors.size()-n; + final int diff = executors.size()-n; if (diff>0) { // we have too many executors // send signal to all idle executors to potentially kill them off - for( Executor e : executors ) - if(e.isIdle()) - e.interrupt(); + // need the Queue maintenance lock held to prevent concurrent job assignment on the idle executors + Queue.withLock(new Runnable() { + @Override + public void run() { + for( Executor e : executors ) + if(e.isIdle()) + e.interrupt(); + } + }); } if (diff<0) { @@ -717,9 +744,16 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces availableNumbers.remove(executor.getNumber()); for (Integer number : availableNumbers) { - Executor e = new Executor(this, number); - executors.add(e); + /* There may be busy executors with higher index, so only + fill up until numExecutors is reached. + Extra executors will call removeExecutor(...) and that + will create any necessary executors from #0 again. */ + if (executors.size() < numExecutors) { + Executor e = new Executor(this, number); + executors.add(e); + } } + } /** @@ -861,7 +895,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces } public String getSearchUrl() { - return "computer/"+nodeName; + return getUrl(); } /** @@ -914,7 +948,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(); @@ -929,7 +963,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); @@ -974,7 +1008,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces * @since 1.300 * @return * null if the host name cannot be computed (for example because this computer is offline, - * because the slave is behind the firewall, etc.) + * because the slave is behind the firewall, etc.) */ public String getHostName() throws IOException, InterruptedException { if(hostNameCached) @@ -1023,7 +1057,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces oneOffExecutors.remove(e); } - private static class ListPossibleNames implements Callable,IOException> { + private static class ListPossibleNames extends MasterToSlaveCallable,IOException> { public List call() throws IOException { List names = new ArrayList(); @@ -1053,26 +1087,17 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces private static final long serialVersionUID = 1L; } - private static class GetFallbackName implements Callable { + private static class GetFallbackName extends MasterToSlaveCallable { public String call() throws IOException { return System.getProperty("host.name"); } 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")))); // // @@ -1090,14 +1115,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); @@ -1105,13 +1129,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(); } @@ -1142,7 +1165,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces w.close(); } - private static final class DumpExportTableTask implements Callable { + private static final class DumpExportTableTask extends MasterToSlaveCallable { public String call() throws IOException { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); @@ -1180,7 +1203,7 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces String name = Util.fixEmptyAndTrim(req.getSubmittedForm().getString("name")); Jenkins.checkGoodName(name); - + Node node = getNode(); if (node == null) { throw new ServletException("No such node " + nodeName); @@ -1203,7 +1226,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")) { @@ -1225,7 +1252,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"); } @@ -1250,6 +1278,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(); @@ -1292,11 +1321,11 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces * Gets the current {@link Computer} that the build is running. * This method only works when called during a build, such as by * {@link hudson.tasks.Publisher}, {@link hudson.tasks.BuildWrapper}, etc. + * @return the {@link Computer} associated with {@link Executor#currentExecutor}, or (consistently as of 1.591) null if not on an executor thread */ - public static Computer currentComputer() { + public static @Nullable Computer currentComputer() { Executor e = Executor.currentExecutor(); - // If no executor then must be on master node - return e != null ? e.getOwner() : Jenkins.getInstance().toComputer(); + return e != null ? e.getOwner() : null; } /** @@ -1304,9 +1333,13 @@ public /*transient*/ abstract class Computer extends Actionable implements Acces * scheduling that does not overlap with being offline. * * @return {@code true} if the computer is accepting tasks + * @see hudson.slaves.RetentionStrategy#isAcceptingTasks(Computer) + * @see hudson.model.Node#isAcceptingTasks() */ + @OverrideMustInvoke(When.ANYTIME) public boolean isAcceptingTasks() { - return true; + final Node node = getNode(); + return getRetentionStrategy().isAcceptingTasks(this) && (node == null || node.isAcceptingTasks()); } /** @@ -1354,7 +1387,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..f7def3d04f2321cd73a179015d06508c1c593aff 100644 --- a/core/src/main/java/hudson/model/ComputerPanelBox.java +++ b/core/src/main/java/hudson/model/ComputerPanelBox.java @@ -1,8 +1,10 @@ package hudson.model; +import hudson.ExtensionList; 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 +39,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: ExtensionList.lookup(ComputerPanelBox.class)){ box.setComputer(computer); boxs.add(box); } diff --git a/core/src/main/java/hudson/model/ComputerPinger.java b/core/src/main/java/hudson/model/ComputerPinger.java index 6dd158fdebf761c7a6f56f47c29fa24a08ba265d..d924ab63b4fea091b3eeccd7033044f0addb2016 100644 --- a/core/src/main/java/hudson/model/ComputerPinger.java +++ b/core/src/main/java/hudson/model/ComputerPinger.java @@ -27,7 +27,7 @@ public abstract class ComputerPinger implements ExtensionPoint { * Get all registered instances. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(ComputerPinger.class); + return ExtensionList.lookup(ComputerPinger.class); } /** diff --git a/core/src/main/java/hudson/model/ComputerSet.java b/core/src/main/java/hudson/model/ComputerSet.java index ea455a09ac2a7d209d6bc8c8c34655f7a3502838..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; @@ -54,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. *

@@ -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..41327d009d337140fc56d058ede288abc286aae7 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 @@ -448,6 +452,10 @@ public class DependencyGraph implements Comparator { hash = 23 * hash + this.downstream.hashCode(); return hash; } + + @Override public String toString() { + return super.toString() + "[" + upstream + "->" + downstream + "]"; + } } /** 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..bb6eeb2126d0a32732b8039b745b341ec299faa4 100644 --- a/core/src/main/java/hudson/model/Descriptor.java +++ b/core/src/main/java/hudson/model/Descriptor.java @@ -28,6 +28,7 @@ import hudson.PluginWrapper; import hudson.RelativePath; import hudson.XmlFile; import hudson.BulkChange; +import hudson.ExtensionList; import hudson.Util; import hudson.model.listeners.SaveableListener; import hudson.util.FormApply; @@ -937,7 +938,7 @@ public abstract class Descriptor> implements Saveable { } public static @CheckForNull Descriptor find(String className) { - return find(Jenkins.getInstance().getExtensionList(Descriptor.class),className); + return find(ExtensionList.lookup(Descriptor.class),className); } public static final class FormException extends Exception implements HttpResponse { @@ -967,7 +968,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..0a435c6fda0777b070e0b3b24814767bbd800fe1 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. * @@ -37,7 +42,7 @@ public abstract class DescriptorVisibilityFilter implements ExtensionPoint { public abstract boolean filter(Object context, Descriptor descriptor); public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(DescriptorVisibilityFilter.class); + return ExtensionList.lookup(DescriptorVisibilityFilter.class); } public static List apply(Object context, Iterable source) { @@ -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 7fd4dfd6613bdf418873625f059ae43f725aa611..cf803fcdc5f8e4648a7aaef5779818024eb15609 100644 --- a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java +++ b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java @@ -25,14 +25,6 @@ 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 java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -45,11 +37,19 @@ 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.security.MasterToSlaveCallable; 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) { @@ -284,7 +284,12 @@ public final class DirectoryBrowserSupport implements HttpResponse { boolean view = rest.equals("*view*"); if(rest.equals("*fingerprint*")) { - rsp.forward(Jenkins.getInstance().getFingerprint(Util.getDigestOf(baseFile.open())), "/", req); + InputStream fingerprintInput = baseFile.open(); + try { + rsp.forward(Jenkins.getInstance().getFingerprint(Util.getDigestOf(fingerprintInput)), "/", req); + } finally { + fingerprintInput.close(); + } return; } @@ -340,6 +345,7 @@ 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)) { String relativePath; if (glob.length() == 0) { @@ -348,11 +354,20 @@ public final class DirectoryBrowserSupport implements HttpResponse { } else { relativePath = n; } - ZipEntry e = new ZipEntry(relativePath); + // In ZIP archives "All slashes MUST be forward slashes" (http://pkware.com/documents/casestudies/APPNOTE.TXT) + // TODO On Linux file names can contain backslashes which should not treated as file separators. + // Unfortunately, only the file separator char of the master is known (File.separatorChar) + // but not the file separator char of the (maybe remote) "dir". + ZipEntry e = new ZipEntry(relativePath.replace('\\', '/')); 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(); @@ -414,6 +429,13 @@ public final class DirectoryBrowserSupport implements HttpResponse { return isFolder?"folder-error.png":"text-error.png"; } + public String getIconClassName() { + if (isReadable) + return isFolder?"icon-folder":"icon-text"; + else + return isFolder?"icon-folder-error":"icon-text-error"; + } + public long getSize() { return size; } @@ -426,7 +448,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); } @@ -448,6 +470,17 @@ public final class DirectoryBrowserSupport implements HttpResponse { } } + private static final class BuildChildPaths extends MasterToSlaveCallable>,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..707491dbc18939c70ec4f5d107e1b4f0c9625436 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,15 +312,19 @@ 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()); } /** * Returns all the registered {@link Downloadable}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(Downloadable.class); + return ExtensionList.lookup(Downloadable.class); } /** @@ -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..d6d630bcb56c47eab192fed8f09a078255757301 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,18 +104,18 @@ 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. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(EnvironmentContributor.class); + return ExtensionList.lookup(EnvironmentContributor.class); } /** diff --git a/core/src/main/java/hudson/model/Executor.java b/core/src/main/java/hudson/model/Executor.java index 5d84e17fff7e95d09a50e81b8f0288bd6e3f5b4a..574cfb748608382d4c1b4121baa6b3712482b75b 100644 --- a/core/src/main/java/hudson/model/Executor.java +++ b/core/src/main/java/hudson/model/Executor.java @@ -1,18 +1,18 @@ /* * The MIT License - * + * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Brian Westrich, Red Hat, Inc., Stephen Connolly, 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 @@ -58,16 +58,20 @@ import java.util.logging.Logger; import static hudson.model.queue.Executables.*; 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; @@ -108,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(); @@ -192,7 +196,6 @@ public class Executor extends Thread implements ModelObject { 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 { @@ -217,6 +220,13 @@ public class Executor extends Thread implements ModelObject { try { workUnit.context.synchronizeStart(); + // this code handles the behavior of null Executables returned + // by tasks. In such case Jenkins starts the workUnit in order + // to report results to console outputs. + if (executable == null) { + throw new Error("The null Executable has been created for "+workUnit+". The task cannot be executed"); + } + if (executable instanceof Actionable) { for (Action action: workUnit.context.actions) { ((Actionable) executable).addAction(action); @@ -273,13 +283,13 @@ public class Executor extends Thread implements ModelObject { } /** - * Returns the current {@link hudson.model.Queue.Task} this executor is running. + * Returns the current build this executor is running. * * @return * null if the executor is idle. */ @Exported - public Queue.Executable getCurrentExecutable() { + public @CheckForNull Queue.Executable getCurrentExecutable() { return executable; } @@ -344,6 +354,15 @@ public class Executor extends Thread implements ModelObject { return executable!=null; } + /** + * Check if executor is ready to accept tasks. + * This method becomes the critical one since 1.536, which introduces the + * on-demand creation of executor threads. The entire logic should use + * this method instead of {@link #isAlive()}, because it provides wrong + * information for non-started threads. + * @return True if the executor is available for tasks + * @since 1.536 + */ public boolean isActive() { return !started || isAlive(); } @@ -491,9 +510,10 @@ public class Executor extends Thread implements ModelObject { /** * Stops the current build. - * + * * @since 1.489 */ + @RequirePOST public HttpResponse doStop() { Queue.Executable e = executable; if(e!=null) { @@ -506,6 +526,7 @@ public class Executor extends Thread implements ModelObject { /** * Throws away this executor and get a new one. */ + @RequirePOST public HttpResponse doYank() { Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER); if (isAlive()) @@ -522,7 +543,7 @@ public class Executor extends Thread implements ModelObject { return e!=null && Tasks.getOwnerTaskOf(getParentOf(e)).hasAbortPermission(); } - public Computer getOwner() { + public @Nonnull Computer getOwner() { return owner; } @@ -547,7 +568,7 @@ public class Executor extends Thread implements ModelObject { /** * Creates a proxy object that executes the callee in the context that impersonates - * this executor. Useful to export an object to a remote channel. + * this executor. Useful to export an object to a remote channel. */ public T newImpersonatingProxy(Class type, T core) { return new InterceptingProxy() { @@ -566,12 +587,12 @@ public class Executor extends Thread implements ModelObject { /** * Returns the executor of the current thread or null if current thread is not an executor. */ - public static Executor currentExecutor() { + public static @CheckForNull Executor currentExecutor() { Thread t = Thread.currentThread(); if (t instanceof Executor) return (Executor) t; return IMPERSONATION.get(); } - + /** * Returns the estimated duration for the executable. * Protects against {@link AbstractMethodError}s if the {@link Executable} implementation diff --git a/core/src/main/java/hudson/model/ExecutorListener.java b/core/src/main/java/hudson/model/ExecutorListener.java index 0588c834aae81e1772b437e70bd8a535acd8d0b1..3c5e221f17f277d8bc2a9c53a3e587e36039f9cd 100644 --- a/core/src/main/java/hudson/model/ExecutorListener.java +++ b/core/src/main/java/hudson/model/ExecutorListener.java @@ -23,11 +23,13 @@ */ package hudson.model; +import hudson.slaves.SlaveComputer; + /** - * A listener for task related events from Executors -* + * A listener for task related events from executors. + * A {@link Computer#getRetentionStrategy} or {@link SlaveComputer#getLauncher} may implement this interface. * @author Stephen Connolly -* @since 17-Jun-2008 18:58:12 +* @since 1.312 */ public interface ExecutorListener { @@ -47,7 +49,7 @@ public interface ExecutorListener { void taskCompleted(Executor executor, Queue.Task task, long durationMS); /** - * Called whenever a task is completed without any problems by an executor. + * Called whenever a task is completed with some problems by an executor. * @param executor The executor. * @param task The task. * @param durationMS The number of milliseconds that the task took to complete. diff --git a/core/src/main/java/hudson/model/FileParameterValue.java b/core/src/main/java/hudson/model/FileParameterValue.java index e5bfda9881329b83290debe7ecea5aecdef44a30..930eb41fd8ae4d7d0ca8c0c28ae9b6540cbad91b 100644 --- a/core/src/main/java/hudson/model/FileParameterValue.java +++ b/core/src/main/java/hudson/model/FileParameterValue.java @@ -39,7 +39,9 @@ import java.io.UnsupportedEncodingException; import javax.servlet.ServletException; import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemHeaders; import org.apache.commons.fileupload.disk.DiskFileItem; +import org.apache.commons.fileupload.util.FileItemHeadersImpl; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; @@ -96,11 +98,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); } @@ -133,7 +140,7 @@ public class FileParameterValue extends ParameterValue { return new BuildWrapper() { @Override public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { - if (!StringUtils.isEmpty(location)) { + if (!StringUtils.isEmpty(location) && !StringUtils.isEmpty(file.getName())) { listener.getLogger().println("Copying file to "+location); FilePath locationFilePath = build.getWorkspace().child(location); locationFilePath.getParent().mkdirs(); @@ -155,7 +162,8 @@ public class FileParameterValue extends ParameterValue { } /** - * In practice this will always be false, since location should be unique. + * Compares file parameters (existing files will be considered as different). + * @since 1.586 Function has been modified in order to avoid JENKINS-19017 issue (wrong merge of builds in the queue). */ @Override public boolean equals(Object obj) { @@ -166,12 +174,13 @@ public class FileParameterValue extends ParameterValue { if (getClass() != obj.getClass()) return false; FileParameterValue other = (FileParameterValue) obj; - if (location == null) { - if (other.location != null) - return false; - } else if (!location.equals(other.location)) - return false; - return true; + + if (location == null && other.location == null) + return true; // Consider null parameters as equal + + //TODO: check fingerprints or checksums to improve the behavior (JENKINS-25211) + // Return false even if files are equal + return false; } @Override @@ -197,12 +206,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 +266,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,8 +307,18 @@ public class FileParameterValue extends ParameterValue { public void setFormField(boolean state) { } + @Deprecated public OutputStream getOutputStream() throws IOException { return new FileOutputStream(file); } + + @Override + public FileItemHeaders getHeaders() { + return new FileItemHeadersImpl(); + } + + @Override + public void setHeaders(FileItemHeaders headers) { + } } } diff --git a/core/src/main/java/hudson/model/Fingerprint.java b/core/src/main/java/hudson/model/Fingerprint.java index 0e91bf7cf2ec76fcf634c3bf47799ba1a151013a..22fac5bb8db1ffe4c0fc8daaa99474b5e0c755fa 100644 --- a/core/src/main/java/hudson/model/Fingerprint.java +++ b/core/src/main/java/hudson/model/Fingerprint.java @@ -24,6 +24,7 @@ package hudson.model; import com.google.common.collect.ImmutableList; +import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; @@ -38,6 +39,7 @@ import hudson.BulkChange; import hudson.Extension; import hudson.model.listeners.ItemListener; import hudson.model.listeners.SaveableListener; +import hudson.security.ACL; import hudson.util.AtomicFileWriter; import hudson.util.HexBinaryConverter; import hudson.util.Iterators; @@ -119,8 +121,9 @@ public class Fingerprint implements ModelObject, Saveable { * Gets the {@link Job} that this pointer points to, * or null if such a job no longer exists. */ - public AbstractProject getJob() { - return Jenkins.getInstance().getItemByFullName(name,AbstractProject.class); + @WithBridgeMethods(value=AbstractProject.class, castRequired=true) + public Job getJob() { + return Jenkins.getInstance().getItemByFullName(name, Job.class); } /** @@ -727,9 +730,16 @@ 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(final Item item, final String oldName, final String newName) { + ACL.impersonate(ACL.SYSTEM, new Runnable() { + @Override public void run() { + locationChanged(item, oldName, newName); + } + }); + } + private void locationChanged(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) { @@ -894,7 +904,15 @@ public class Fingerprint implements ModelObject, Saveable { return r; } + @Deprecated public synchronized void add(AbstractBuild b) throws IOException { + addFor((Run) b); + } + + /** + * @since 1.577 + */ + public synchronized void addFor(Run b) throws IOException { add(b.getParent().getFullName(), b.getNumber()); } @@ -1075,6 +1093,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..98fa48b72c5a594914b6173012088610e580778f 100644 --- a/core/src/main/java/hudson/model/FingerprintCleanupThread.java +++ b/core/src/main/java/hudson/model/FingerprintCleanupThread.java @@ -24,12 +24,12 @@ package hudson.model; import hudson.Extension; +import hudson.ExtensionList; 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; /** @@ -58,7 +58,7 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork { } private static FingerprintCleanupThread getInstance() { - return Jenkins.getInstance().getExtensionList(AsyncPeriodicWork.class).get(FingerprintCleanupThread.class); + return ExtensionList.lookup(AsyncPeriodicWork.class).get(FingerprintCleanupThread.class); } public void execute(TaskListener listener) { @@ -72,7 +72,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 +81,7 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork { } } - logger.log(Level.INFO, "Cleaned up "+numFiles+" records"); + listener.getLogger().println("Cleaned up "+numFiles+" records"); } /** @@ -97,22 +97,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/FullDuplexHttpChannel.java b/core/src/main/java/hudson/model/FullDuplexHttpChannel.java index c75b30d5389f1392c668041cf1671f6f277f72c9..6372b60ed21b917ec65346f035e188f8f5078e87 100644 --- a/core/src/main/java/hudson/model/FullDuplexHttpChannel.java +++ b/core/src/main/java/hudson/model/FullDuplexHttpChannel.java @@ -28,6 +28,8 @@ import hudson.remoting.PingThread; import hudson.remoting.Channel.Mode; import hudson.util.ChunkedOutputStream; import hudson.util.ChunkedInputStream; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; @@ -36,6 +38,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; @@ -78,9 +82,14 @@ abstract public class FullDuplexHttpChannel { out.write("Starting HTTP duplex channel".getBytes()); out.flush(); - // wait until we have the other channel - while(upload==null) - wait(); + {// wait until we have the other channel + long end = System.currentTimeMillis() + CONNECTION_TIMEOUT; + while (upload == null && System.currentTimeMillis() { // These are now 0-20, 21-40, 41-60, 61-80, 81+ but filenames unchanged for compatibility - private static final String HEALTH_OVER_80 = "health-80plus.png"; - private static final String HEALTH_61_TO_80 = "health-60to79.png"; - private static final String HEALTH_41_TO_60 = "health-40to59.png"; - private static final String HEALTH_21_TO_40 = "health-20to39.png"; - private static final String HEALTH_0_TO_20 = "health-00to19.png"; - private static final String HEALTH_UNKNOWN = "empty.png"; + private static final String HEALTH_OVER_80 = "icon-health-80plus"; + private static final String HEALTH_61_TO_80 = "icon-health-60to79"; + private static final String HEALTH_41_TO_60 = "icon-health-40to59"; + private static final String HEALTH_21_TO_40 = "icon-health-20to39"; + private static final String HEALTH_0_TO_20 = "icon-health-00to19"; + + private static final String HEALTH_OVER_80_IMG = "health-80plus.png"; + private static final String HEALTH_61_TO_80_IMG = "health-60to79.png"; + private static final String HEALTH_41_TO_60_IMG = "health-40to59.png"; + private static final String HEALTH_21_TO_40_IMG = "health-20to39.png"; + private static final String HEALTH_0_TO_20_IMG = "health-00to19.png"; + private static final String HEALTH_UNKNOWN_IMG = "empty.png"; + + private static final Map iconIMGToClassMap = new HashMap(); + static { + iconIMGToClassMap.put(HEALTH_OVER_80_IMG, HEALTH_OVER_80); + iconIMGToClassMap.put(HEALTH_61_TO_80_IMG, HEALTH_61_TO_80); + iconIMGToClassMap.put(HEALTH_41_TO_60_IMG, HEALTH_41_TO_60); + iconIMGToClassMap.put(HEALTH_21_TO_40_IMG, HEALTH_21_TO_40); + iconIMGToClassMap.put(HEALTH_0_TO_20_IMG, HEALTH_0_TO_20); + } /** * The percentage health score (from 0 to 100 inclusive). */ private int score; + /** + * Icon class. + */ + private String iconClassName; + /** * The path to the icon corresponding to this health score or null to use the default icon * corresponding to the current health score. @@ -120,17 +141,28 @@ public class HealthReport implements Serializable, Comparable { */ public HealthReport(int score, String iconUrl, Localizable description) { this.score = score; + if (score <= 20) { + this.iconClassName = HEALTH_0_TO_20; + } else if (score <= 40) { + this.iconClassName = HEALTH_21_TO_40; + } else if (score <= 60) { + this.iconClassName = HEALTH_41_TO_60; + } else if (score <= 80) { + this.iconClassName = HEALTH_61_TO_80; + } else { + this.iconClassName = HEALTH_OVER_80; + } if (iconUrl == null) { if (score <= 20) { - this.iconUrl = HEALTH_0_TO_20; + this.iconUrl = HEALTH_0_TO_20_IMG; } else if (score <= 40) { - this.iconUrl = HEALTH_21_TO_40; + this.iconUrl = HEALTH_21_TO_40_IMG; } else if (score <= 60) { - this.iconUrl = HEALTH_41_TO_60; + this.iconUrl = HEALTH_41_TO_60_IMG; } else if (score <= 80) { - this.iconUrl = HEALTH_61_TO_80; + this.iconUrl = HEALTH_61_TO_80_IMG; } else { - this.iconUrl = HEALTH_OVER_80; + this.iconUrl = HEALTH_OVER_80_IMG; } } else { this.iconUrl = iconUrl; @@ -166,7 +198,7 @@ public class HealthReport implements Serializable, Comparable { * Create a new HealthReport. */ public HealthReport() { - this(100, HEALTH_UNKNOWN, Messages._HealthReport_EmptyString()); + this(100, HEALTH_UNKNOWN_IMG, Messages._HealthReport_EmptyString()); } /** @@ -188,6 +220,8 @@ public class HealthReport implements Serializable, Comparable { this.score = score; } + + /** * Getter for property 'iconUrl'. * @@ -198,6 +232,16 @@ public class HealthReport implements Serializable, Comparable { return iconUrl; } + /** + * Get health status icon class. + * + * @return The health status icon class. + */ + @Exported + public String getIconClassName() { + return iconClassName; + } + /** * Get's the iconUrl relative to the hudson root url, for the correct size. * @@ -206,7 +250,7 @@ public class HealthReport implements Serializable, Comparable { */ public String getIconUrl(String size) { if (iconUrl == null) { - return Jenkins.RESOURCE_PATH + "/images/" + size + "/" + HEALTH_UNKNOWN; + return Jenkins.RESOURCE_PATH + "/images/" + size + "/" + HEALTH_UNKNOWN_IMG; } if (iconUrl.startsWith("/")) { return iconUrl.replace("/32x32/", "/" + size + "/"); @@ -318,6 +362,10 @@ public class HealthReport implements Serializable, Comparable { hr.localizibleDescription = new NonLocalizable(hr.description == null ? "" : hr.description); OldDataMonitor.report(context, "1.256"); } + + if (hr.iconClassName == null && hr.iconUrl != null && iconIMGToClassMap.containsKey(hr.iconUrl)) { + hr.iconClassName = iconIMGToClassMap.get(hr.iconUrl); + } } } } diff --git a/core/src/main/java/hudson/model/Hudson.java b/core/src/main/java/hudson/model/Hudson.java index aff8df4341c0faae5b7b239e14b06476c43e958b..c4b6cf4a61554a50b2f082d111feaf9d2ec9c78a 100644 --- a/core/src/main/java/hudson/model/Hudson.java +++ b/core/src/main/java/hudson/model/Hudson.java @@ -52,6 +52,7 @@ import java.util.List; import java.util.Map; import static hudson.Util.fixEmpty; +import javax.annotation.CheckForNull; public class Hudson extends Jenkins { @@ -67,9 +68,10 @@ 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() { + public static @CheckForNull 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..726796fa8e673bb118386237d4bbcefcaeb15570 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 @@ -224,8 +224,9 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont Permission DELETE = new Permission(PERMISSIONS, "Delete", Messages._Item_DELETE_description(), Permission.DELETE, PermissionScope.ITEM); Permission CONFIGURE = new Permission(PERMISSIONS, "Configure", Messages._Item_CONFIGURE_description(), Permission.CONFIGURE, PermissionScope.ITEM); Permission READ = new Permission(PERMISSIONS, "Read", Messages._Item_READ_description(), Permission.READ, PermissionScope.ITEM); - Permission DISCOVER = new Permission(PERMISSIONS, "Discover", Messages._AbstractProject_DiscoverPermission_Description(), Permission.READ, PermissionScope.ITEM); + Permission DISCOVER = new Permission(PERMISSIONS, "Discover", Messages._AbstractProject_DiscoverPermission_Description(), READ, PermissionScope.ITEM); Permission EXTENDED_READ = new Permission(PERMISSIONS,"ExtendedRead", Messages._AbstractProject_ExtendedReadPermission_Description(), CONFIGURE, Boolean.getBoolean("hudson.security.ExtendedReadPermission"), new PermissionScope[]{PermissionScope.ITEM}); + // TODO the following really belong in Job, not Item, but too late to move since the owner.name is encoded in the ID: Permission BUILD = new Permission(PERMISSIONS, "Build", Messages._AbstractProject_BuildPermission_Description(), Permission.UPDATE, PermissionScope.ITEM); Permission WORKSPACE = new Permission(PERMISSIONS, "Workspace", Messages._AbstractProject_WorkspacePermission_Description(), Permission.READ, PermissionScope.ITEM); Permission WIPEOUT = new Permission(PERMISSIONS, "WipeOut", Messages._AbstractProject_WipeOutPermission_Description(), null, Functions.isWipeOutPermissionEnabled(), new PermissionScope[]{PermissionScope.ITEM}); diff --git a/core/src/main/java/hudson/model/ItemGroup.java b/core/src/main/java/hudson/model/ItemGroup.java index a87814eb9d8a8423e64c0c8401958084c48f2bfb..ace90e16b4354ec2f98dd022e7dce4d9cced37f6 100644 --- a/core/src/main/java/hudson/model/ItemGroup.java +++ b/core/src/main/java/hudson/model/ItemGroup.java @@ -23,10 +23,12 @@ */ package hudson.model; +import hudson.model.listeners.ItemListener; import java.io.IOException; import java.util.Collection; import java.io.File; import javax.annotation.CheckForNull; +import org.acegisecurity.AccessDeniedException; /** * Represents a grouping inherent to a kind of {@link Item}s. @@ -66,8 +68,10 @@ public interface ItemGroup extends PersistenceRoot, ModelObject /** * Gets the {@link Item} inside this group that has a given name, or null if it does not exist. + * @throws AccessDeniedException if the current user has {@link Item#DISCOVER} but not {@link Item#READ} on this item + * @return an item whose {@link Item#getName} is {@code name} and whose {@link Item#getParent} is {@code this}, or null if there is no such item, or there is but the current user lacks both {@link Item#DISCOVER} and {@link Item#READ} on it */ - @CheckForNull T getItem(String name); + @CheckForNull T getItem(String name) throws AccessDeniedException; /** * Assigns the {@link Item#getRootDir() root directory} for children. @@ -76,6 +80,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 0c5a6bdb3f031a121ca37112c788b80ea81437b8..d34b5e7f9354f87cba6aa1a7513e2007149e2309 100644 --- a/core/src/main/java/hudson/model/ItemGroupMixIn.java +++ b/core/src/main/java/hudson/model/ItemGroupMixIn.java @@ -24,6 +24,7 @@ package hudson.model; import hudson.Util; +import hudson.XmlFile; import hudson.model.listeners.ItemListener; import hudson.security.AccessControlled; import hudson.util.CopyOnWriteMap; @@ -40,6 +41,9 @@ 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; +import jenkins.security.NotReallyRoleSensitiveCallable; /** * 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); } } @@ -197,6 +213,7 @@ public abstract class ItemGroupMixIn { @SuppressWarnings({"unchecked"}) public synchronized T copy(T src, String name) throws IOException { acl.checkPermission(Item.CREATE); + src.checkPermission(Item.EXTENDED_READ); T result = (T)createProject(src.getDescriptor(),name,false); @@ -204,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 NotReallyRoleSensitiveCallable() { + @Override public T call() throws IOException { + return (T) Items.load(parent, rootDir); + } + }); result.onCopiedFrom(src); add(result); @@ -223,20 +240,24 @@ public abstract class ItemGroupMixIn { 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 NotReallyRoleSensitiveCallable() { + @Override public TopLevelItem call() throws IOException { + return (TopLevelItem) Items.load(parent, dir); + } + }); add(result); ItemListener.fireOnCreated(result); @@ -245,7 +266,7 @@ 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; } } @@ -257,13 +278,9 @@ public abstract class ItemGroupMixIn { 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 5ca561beaa59cd96aee46e8cc0c95b266c918139..0358e4471aeda42dc4f67f746638c9efffa0632f 100644 --- a/core/src/main/java/hudson/model/Items.java +++ b/core/src/main/java/hudson/model/Items.java @@ -26,10 +26,9 @@ 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; @@ -39,8 +38,16 @@ 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}. @@ -61,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. */ @@ -149,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 @@ -170,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); } @@ -179,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. * @@ -246,39 +302,33 @@ 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; - } - if (o2 == null) { - return -1; + String name(Item i) { + String n = i.getName(); + if (i instanceof ItemGroup) { + n += '/'; } - 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); + } + } } /** @@ -300,6 +350,42 @@ public class Items { 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); + item.movedTo(destination, newItem, destDir); + ItemListener.fireLocationChange(newItem, oldFullName); + return newItem; + } + /** * Used to load/save job configuration. * @@ -315,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..a06bac9c649dc11cade89b4a9b5aa2bd0342c05b 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. @@ -55,6 +52,13 @@ import org.kohsuke.stapler.QueryParameter; * @author Kohsuke Kawaguchi */ public final class JDK extends ToolInstallation implements NodeSpecific, EnvironmentSpecific { + + /** + * Name of the “default JDK”, meaning no specific JDK selected. + * @since 1.577 + */ + public static final String DEFAULT_NAME = "(Default)"; + /** * @deprecated since 2009-02-25 */ @@ -175,16 +179,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 +188,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..7020de7ee3471361fb6a011ba5d462e676df9a54 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; @@ -62,6 +63,7 @@ import hudson.widgets.HistoryWidget; import hudson.widgets.HistoryWidget.Adapter; import hudson.widgets.Widget; import jenkins.model.BuildDiscarder; +import jenkins.model.DirectlyModifiableTopLevelItemGroup; import jenkins.model.Jenkins; import jenkins.model.ProjectNamingStrategy; import jenkins.security.HexStringConfidentialKey; @@ -69,6 +71,7 @@ import jenkins.util.io.OnMaster; import net.sf.json.JSONException; import net.sf.json.JSONObject; +import org.apache.commons.io.FileUtils; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.CategoryAxis; @@ -95,8 +98,14 @@ import java.io.*; import java.net.URLEncoder; import java.util.*; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import static javax.servlet.http.HttpServletResponse.*; +import jenkins.model.ModelObjectWithChildren; +import jenkins.model.lazy.LazyBuildMixIn; /** * A job is an runnable entity under the monitoring of Hudson. @@ -110,7 +119,7 @@ import static javax.servlet.http.HttpServletResponse.*; * @author Kohsuke Kawaguchi */ public abstract class Job, RunT extends Run> - extends AbstractItem implements ExtensionPoint, StaplerOverridable, OnMaster { + extends AbstractItem implements ExtensionPoint, StaplerOverridable, ModelObjectWithChildren, OnMaster { /** * Next build number. Kept in a separate file because this is the only @@ -145,7 +154,7 @@ public abstract class Job, RunT extends Run cachedBuildHealthReports = null; - private boolean keepDependencies; + boolean keepDependencies; /** * List of {@link UserProperty}s configured for this project. @@ -308,6 +317,7 @@ public abstract class Job, RunT extends Run, 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); @@ -608,13 +623,28 @@ 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 +766,7 @@ public abstract class Job, RunT extends Run m = _getRuns().tailMap(n); @@ -776,7 +808,11 @@ public abstract class Job, RunT extends Run, RunT extends Run _getRuns(); @@ -792,11 +829,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 +965,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,14 +1012,23 @@ public abstract class Job, RunT extends Run, 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 +1196,11 @@ public abstract class Job, RunT extends Run, RunT extends Run, RunT extends Run { @@ -1269,7 +1322,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 +1398,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/Label.java b/core/src/main/java/hudson/model/Label.java index 837987f0a88eed627951ea7e29b28513c5e59491..cdfcfb5e864ac9f6d94c6c787506d0af5a0c770f 100644 --- a/core/src/main/java/hudson/model/Label.java +++ b/core/src/main/java/hudson/model/Label.java @@ -1,18 +1,18 @@ /* * 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 @@ -71,7 +71,7 @@ import com.thoughtworks.xstream.io.HierarchicalStreamReader; /** * Group of {@link Node}s. - * + * * @author Kohsuke Kawaguchi * @see Jenkins#getLabels() * @see Jenkins#getLabel(String) @@ -142,6 +142,13 @@ public abstract class Label extends Actionable implements Comparable

@@ -410,7 +417,7 @@ public abstract class Label extends Actionable implements Comparable

* As a special case, {@link Jenkins} extends from here. @@ -82,6 +78,10 @@ import org.kohsuke.stapler.export.Exported; * Nodes are persisted objects that capture user configurations, and instances get thrown away and recreated whenever * the configuration changes. Running state of nodes are captured by {@link Computer}s. * + *

+ * There is no URL binding for {@link Node}. {@link Computer} and {@link TransientComputerActionFactory} must + * be used to associate new {@link Action}s to slaves. + * * @author Kohsuke Kawaguchi * @see NodeMonitor * @see NodeDescriptor @@ -103,7 +103,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 +121,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 @@ -141,7 +145,7 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable * Returns a {@link Launcher} for executing programs on this node. * *

- * The callee must call {@link Launcher#decorateFor(Node)} before returning to complete the decoration. + * The callee must call {@link Launcher#decorateFor(Node)} before returning to complete the decoration. */ public abstract Launcher createLauncher(TaskListener listener); @@ -177,7 +181,7 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable /** * Gets the current channel, if the node is connected and online, or null. * - * This is just a convenience method for {@link Computer#getChannel()} with null check. + * This is just a convenience method for {@link Computer#getChannel()} with null check. */ public final @CheckForNull VirtualChannel getChannel() { Computer c = toComputer(); @@ -190,6 +194,20 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable */ protected abstract Computer createComputer(); + /** + * Returns {@code true} if the node is accepting tasks. Needed to allow slaves programmatic suspension of task + * scheduling that does not overlap with being offline. Called by {@link Computer#isAcceptingTasks()}. + * This method is distinct from {@link Computer#isAcceptingTasks()} as sometimes the {@link Node} concrete + * class may not have control over the {@link hudson.model.Computer} concrete class associated with it. + * + * @return {@code true} if the node is accepting tasks. + * @see Computer#isAcceptingTasks() + * @since 1.586 + */ + public boolean isAcceptingTasks() { + return true; + } + /** * Let Nodes be aware of the lifecycle of their own {@link Computer}. */ @@ -202,7 +220,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); } } @@ -273,10 +291,10 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable /** * Returns the manually configured label for a node. The list of assigned - * and dynamically determined labels is available via + * and dynamically determined labels is available via * {@link #getAssignedLabels()} and includes all labels that have been * manually configured. - * + * * Mainly for form binding. */ public abstract String getLabelString(); @@ -345,7 +363,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 +388,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 +401,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. @@ -409,11 +426,11 @@ public abstract class Node extends AbstractModelObject implements Reconfigurable public List getNodePropertyDescriptors() { return NodeProperty.for_(this); } - + public ACL getACL() { return Jenkins.getInstance().getAuthorizationStrategy().getACL(this); } - + public final void checkPermission(Permission permission) { getACL().checkPermission(permission); } diff --git a/core/src/main/java/hudson/model/OverallLoadStatistics.java b/core/src/main/java/hudson/model/OverallLoadStatistics.java index e07593b3cf1f6ca307357edc19c372c90bbbf100..7dea1ad9561beb2fce14bffdc35f4a44705c12b9 100644 --- a/core/src/main/java/hudson/model/OverallLoadStatistics.java +++ b/core/src/main/java/hudson/model/OverallLoadStatistics.java @@ -1,18 +1,18 @@ /* * The MIT License - * + * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, 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 @@ -70,7 +70,7 @@ public class OverallLoadStatistics extends LoadStatistics { /** * When drawing the overall load statistics, use the total queue length, - * not {@link #queueLength}, which just shows jobs that are to be run on the master. + * not {@link #queueLength}, which just shows jobs that are to be run on the master. */ protected TrendChart createOverallTrendChart(TimeScale timeScale) { return MultiStageTimeSeries.createTrendChart(timeScale,busyExecutors,totalExecutors,queueLength); 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 09aebf7ebfac6ac176b86b06621be09e0424d2a5..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; @@ -46,6 +45,8 @@ 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. @@ -56,7 +57,7 @@ import static com.google.common.collect.Sets.newHashSet; * 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; @@ -75,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. */ @@ -101,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(); } @@ -119,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; } @@ -165,7 +176,9 @@ 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 */ + @Nonnull public ParametersAction createUpdated(Collection overrides) { if(overrides == null) { return new ParametersAction(parameters); @@ -174,10 +187,12 @@ public class ParametersAction implements Action, Iterable, Queue Set names = newHashSet(); for(ParameterValue v : overrides) { + if (v == null) continue; names.add(v.getName()); } for (ParameterValue v : parameters) { + if (v == null) continue; if (!names.contains(v.getName())) { combinedParameters.add(v); } @@ -186,8 +201,14 @@ public class ParametersAction implements Action, Iterable, Queue return new ParametersAction(combinedParameters); } - public ParametersAction merge(ParametersAction overrides) { - if(overrides == null) { + /* + * 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()); 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..d66ef11d48ac685f8620204aa7afcd6f04b15f5e 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()); /** @@ -86,7 +93,15 @@ public abstract class PeriodicWork extends SafeTimerTask implements ExtensionPoi * Returns all the registered {@link PeriodicWork}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(PeriodicWork.class); + return ExtensionList.lookup(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 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 60a85fac44397ee4278dc9587437b85cca23d769..65058e65f339a6dc929e5e38ff43f4b9d1c89202 100644 --- a/core/src/main/java/hudson/model/Queue.java +++ b/core/src/main/java/hudson/model/Queue.java @@ -1,19 +1,19 @@ /* * The MIT License - * + * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, * Stephen Connolly, Tom Huybrechts, 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 @@ -65,8 +65,9 @@ import hudson.model.queue.CauseOfBlockage.BecauseLabelIsOffline; import hudson.model.queue.CauseOfBlockage.BecauseNodeIsBusy; import hudson.model.queue.WorkUnitContext; import hudson.security.ACL; +import jenkins.security.QueueItemAuthenticatorProvider; +import jenkins.util.Timer; import hudson.triggers.SafeTimerTask; -import hudson.triggers.Trigger; import hudson.util.TimeUnit2; import hudson.util.XStream2; import hudson.util.ConsistentHash; @@ -91,7 +92,6 @@ 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.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -115,8 +115,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; @@ -148,6 +152,14 @@ import org.kohsuke.stapler.interceptor.RequirePOST; */ @ExportedBean public class Queue extends ResourceController implements Saveable { + + /** + * Defines the refresh period for of the internal cache ({@link #itemsView}). + * Data should be defined in milliseconds, default value - 1000; + * @since 1.577 + */ + private static int CACHE_REFRESH_PERIOD = Integer.getInteger(Queue.class.getName() + ".cacheRefreshPeriod", 1000); + /** * Items that are waiting for its quiet period to pass. * @@ -208,7 +220,7 @@ public class Queue extends ResourceController implements Saveable { long t = System.currentTimeMillis(); long d = expires.get(); if (t>d) {// need to refresh the cache - long next = t+1000; + long next = t+CACHE_REFRESH_PERIOD; if (expires.compareAndSet(d,next)) { // avoid concurrent cache update via CAS. // if the getItems() lock is contended, @@ -280,6 +292,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(); } @@ -306,7 +319,7 @@ public class Queue extends ResourceController implements Saveable { } }); - public Queue(LoadBalancer loadBalancer) { + public Queue(@Nonnull 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. @@ -317,7 +330,7 @@ public class Queue extends ResourceController implements Saveable { return loadBalancer; } - public void setLoadBalancer(LoadBalancer loadBalancer) { + public void setLoadBalancer(@Nonnull LoadBalancer loadBalancer) { if(loadBalancer==null) throw new IllegalArgumentException(); this.loadBalancer = loadBalancer.sanitize(); } @@ -400,7 +413,7 @@ public class Queue extends ResourceController implements Saveable { */ public synchronized void save() { if(BulkChange.contains(this)) return; - + // write out the tasks on the queue ArrayList items = new ArrayList(); for (Item item: getItems()) { @@ -449,12 +462,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()); } @@ -521,8 +531,9 @@ public class Queue extends ResourceController implements Saveable { * * @since 1.311 * @return - * null if this task is already in the queue and therefore the add operation was no-op. - * Otherwise indicates the {@link WaitingItem} object added, although the nature of the queue + * {@link hudson.model.queue.ScheduleResult.Existing} if this task is already in the queue and + * therefore the add operation was no-op. Otherwise {@link hudson.model.queue.ScheduleResult.Created} + * indicates the {@link WaitingItem} object added, although the nature of the queue * is that such {@link Item} only captures the state of the item at a particular moment, * and by the time you inspect the object, some of its information can be already stale. * @@ -540,7 +551,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); @@ -589,7 +600,7 @@ public class Queue extends ResourceController implements Saveable { if (queueUpdated) scheduleMaintenance(); // REVISIT: when there are multiple existing items in the queue that matches the incoming one, - // whether the new one should affect all existing ones or not is debateable. I for myself + // whether the new one should affect all existing ones or not is debatable. I for myself // thought this would only affect one, so the code was bit of surprise, but I'm keeping the current // behaviour. return ScheduleResult.existing(duplicatesInQueue.get(0)); @@ -598,19 +609,19 @@ public class Queue extends ResourceController implements Saveable { /** * @deprecated as of 1.311 - * Use {@link #schedule(Task, int)} + * Use {@link #schedule(Task, int)} */ public synchronized boolean add(Task p, int quietPeriod) { 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]); } /** * @deprecated as of 1.311 - * Use {@link #schedule(Task, int, Action...)} + * Use {@link #schedule(Task, int, Action...)} */ public synchronized boolean add(Task p, int quietPeriod, Action... actions) { return schedule(p, quietPeriod, actions)!=null; @@ -619,7 +630,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(); } @@ -646,15 +657,10 @@ public class Queue extends ResourceController implements Saveable { // use bitwise-OR to make sure that both branches get evaluated all the time return blockedProjects.cancel(p)!=null | buildables.cancel(p)!=null; } - + 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); - - LeftItem li = new LeftItem(item); - li.enter(this); - - return r; + return item.cancel(this); } /** @@ -717,7 +723,7 @@ public class Queue extends ResourceController implements Saveable { public List getApproximateItemsQuickly() { return itemsView.get(); } - + public synchronized Item getItem(int id) { for (Item item: waitingList) if (item.id == id) return item; for (Item item: blockedProjects) if (item.id == id) return item; @@ -739,6 +745,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); @@ -822,11 +830,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; } @@ -834,7 +844,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); } /** @@ -854,8 +864,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; } @@ -871,8 +882,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; } @@ -893,8 +905,9 @@ 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; } @@ -956,6 +969,102 @@ public class Queue extends ResourceController implements Saveable { return !buildables.containsKey(t) && !pendings.containsKey(t); } + /** + * Some operations require to be performed with the {@link Queue} lock held. Use one of these methods rather + * than locking directly on Queue in order to allow for future refactoring. + * @param runnable the operation to perform. + * @since 1.592 + */ + public static void withLock(Runnable runnable) { + final Jenkins jenkins = Jenkins.getInstance(); + final Queue queue = jenkins == null ? null : jenkins.getQueue(); + if (queue == null) { + runnable.run(); + } else { + queue._withLock(runnable); + } + } + + /** + * Some operations require to be performed with the {@link Queue} lock held. Use one of these methods rather + * than locking directly on Queue in order to allow for future refactoring. + * + * @param callable the operation to perform. + * @param the type of return value + * @param the type of exception. + * @return the result of the callable. + * @throws T the exception of the callable + * @since 1.592 + */ + public static V withLock(hudson.remoting.Callable callable) throws T { + final Jenkins jenkins = Jenkins.getInstance(); + final Queue queue = jenkins == null ? null : jenkins.getQueue(); + if (queue == null) { + return callable.call(); + } else { + return queue._withLock(callable); + } + } + + /** + * Some operations require to be performed with the {@link Queue} lock held. Use one of these methods rather + * than locking directly on Queue in order to allow for future refactoring. + * + * @param callable the operation to perform. + * @param the type of return value + * @return the result of the callable. + * @throws Exception if the callable throws an exception. + * @since 1.592 + */ + public static V withLock(java.util.concurrent.Callable callable) throws Exception { + final Jenkins jenkins = Jenkins.getInstance(); + final Queue queue = jenkins == null ? null : jenkins.getQueue(); + if (queue == null) { + return callable.call(); + } else { + return queue._withLock(callable); + } + } + + /** + * Some operations require to be performed with the {@link Queue} lock held. Use one of these methods rather + * than locking directly on Queue in order to allow for future refactoring. + * @param runnable the operation to perform. + * @since 1.592 + */ + protected synchronized void _withLock(Runnable runnable) { + runnable.run(); + } + + /** + * Some operations require to be performed with the {@link Queue} lock held. Use one of these methods rather + * than locking directly on Queue in order to allow for future refactoring. + * + * @param callable the operation to perform. + * @param the type of return value + * @param the type of exception. + * @return the result of the callable. + * @throws T the exception of the callable + * @since 1.592 + */ + protected synchronized V _withLock(hudson.remoting.Callable callable) throws T { + return callable.call(); + } + + /** + * Some operations require to be performed with the {@link Queue} lock held. Use one of these methods rather + * than locking directly on Queue in order to allow for future refactoring. + * + * @param callable the operation to perform. + * @param the type of return value + * @return the result of the callable. + * @throws Exception if the callable throws an exception. + * @since 1.592 + */ + protected synchronized V _withLock(java.util.concurrent.Callable callable) throws Exception { + return callable.call(); + } + /** * Queue maintenance. * @@ -1057,16 +1166,20 @@ public class Queue extends ResourceController implements Saveable { private void makeBuildable(BuildableItem p) { if(Jenkins.FLYWEIGHT_SUPPORT && p.task instanceof FlyweightTask && !ifBlockedByHudsonShutdown(p.task)) { - ConsistentHash hash = new ConsistentHash(new Hash() { - public String hash(Node node) { - return node.getNodeName(); - } - }); + + Jenkins h = Jenkins.getInstance(); + Map hashSource = new HashMap(h.getNodes().size()); + // Even if master is configured with zero executors, we may need to run a flyweight task like MatrixProject on it. - hash.add(h, Math.max(h.getNumExecutors()*100, 1)); - for (Node n : h.getNodes()) - hash.add(n, n.getNumExecutors()*100); + hashSource.put(h, Math.max(h.getNumExecutors() * 100, 1)); + + for (Node n : h.getNodes()) { + hashSource.put(n, n.getNumExecutors() * 100); + } + + ConsistentHash hash = new ConsistentHash(NODE_HASH); + hash.addAll(hashSource); Label lbl = p.getAssignedLabel(); for (Node n : hash.list(p.task.getFullDisplayName())) { @@ -1085,6 +1198,13 @@ public class Queue extends ResourceController implements Saveable { p.enter(this); } + + private static Hash NODE_HASH = new Hash() { + public String hash(Node node) { + return node.getNodeName(); + } + }; + private boolean makePending(BuildableItem p) { // LOGGER.info("Making "+p.task+" pending"); // REMOVE p.isPending = true; @@ -1116,7 +1236,7 @@ public class Queue extends ResourceController implements Saveable { * Marks {@link Task}s that are not affected by the {@linkplain Jenkins#isQuietingDown()} quieting down}, * because these tasks keep other tasks executing. * - * @since 1.336 + * @since 1.336 */ public interface NonBlockingTask extends Task {} @@ -1197,9 +1317,10 @@ public class Queue extends ResourceController implements Saveable { /** * Works just like {@link #checkAbortPermission()} except it indicates the status by a return value, * instead of exception. + * Also used by default for {@link hudson.model.Queue.Item#hasCancelPermission}. */ boolean hasAbortPermission(); - + /** * Returns the URL of this task relative to the context root of the application. * @@ -1211,7 +1332,7 @@ public class Queue extends ResourceController implements Saveable { * URL that ends with '/'. */ String getUrl(); - + /** * True if the task allows concurrent builds, where the same {@link Task} is executed * by multiple executors concurrently on the same or different nodes. @@ -1257,6 +1378,25 @@ public class Queue extends ResourceController implements Saveable { * @see Tasks#getDefaultAuthenticationOf(Queue.Task) */ @Nonnull Authentication getDefaultAuthentication(); + + /** + * This method allows the task to provide the default fallback authentication object to be used + * when {@link QueueItemAuthenticator} fails to authenticate the build. + * + *

+ * When the task execution touches other objects inside Jenkins, the access control is performed + * based on whether this {@link Authentication} is allowed to use them. + * + *

+ * This method was added to an interface after it was created, so plugins built against + * older versions of Jenkins may not have this method implemented. Called private method _getDefaultAuthenticationOf(Task) on {@link Tasks} + * to avoid {@link AbstractMethodError}. + * + * @since 1.592 + * @see QueueItemAuthenticator + * @see Tasks#getDefaultAuthenticationOf(Queue.Task, Queue.Item) + */ + @Nonnull Authentication getDefaultAuthentication(Queue.Item item); } /** @@ -1270,24 +1410,23 @@ 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 */ void run(); - + /** * Estimate of how long will it take to execute this executable. * Measured in milliseconds. - * + * * Please, consider using {@link Executables#getEstimatedDurationFor(Queue.Executable)} * to protected against AbstractMethodErrors! * @@ -1313,7 +1452,7 @@ public class Queue extends ResourceController implements Saveable { */ @Exported public final int id; - + /** * Project to be built. */ @@ -1321,7 +1460,7 @@ public class Queue extends ResourceController implements Saveable { public final Task task; private /*almost final*/ transient FutureImpl future; - + private final long inQueueSince; /** @@ -1345,7 +1484,7 @@ public class Queue extends ResourceController implements Saveable { */ @Exported public boolean isStuck() { return false; } - + /** * Since when is this item in the queue. * @return Unix timestamp @@ -1354,7 +1493,7 @@ public class Queue extends ResourceController implements Saveable { public long getInQueueSince() { return this.inQueueSince; } - + /** * Returns a human readable presentation of how long this item is already in the queue. * E.g. something like '3 minutes 40 seconds' @@ -1376,7 +1515,7 @@ public class Queue extends ResourceController implements Saveable { * If this task needs to be run on a node with a particular label, * return that {@link Label}. Otherwise null, indicating * it can run on anywhere. - * + * *

* This code takes {@link LabelAssignmentAction} into account, then fall back to {@link SubTask#getAssignedLabel()} */ @@ -1388,6 +1527,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. * @@ -1401,6 +1555,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; @@ -1408,7 +1572,7 @@ public class Queue extends ResourceController implements Saveable { this.inQueueSince = System.currentTimeMillis(); for (Action action: actions) addAction(action); } - + protected Item(Task task, List actions, int id, FutureImpl future, long inQueueSince) { this.task = task; this.id = id; @@ -1416,9 +1580,9 @@ public class Queue extends ResourceController implements Saveable { this.inQueueSince = inQueueSince; for (Action action: actions) addAction(action); } - + 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); } /** @@ -1454,21 +1618,22 @@ 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(); } - + + /** + * Checks whether a scheduled item may be canceled. + * @return by default, the same as {@link hudson.model.Queue.Task#hasAbortPermission} + */ public boolean hasCancelPermission() { return task.hasAbortPermission(); } - + public String getDisplayName() { // TODO Auto-generated method stub return null; @@ -1499,12 +1664,12 @@ public class Queue extends ResourceController implements Saveable { */ @Nonnull public Authentication authenticate() { - for (QueueItemAuthenticator auth : QueueItemAuthenticatorConfiguration.get().getAuthenticators()) { + for (QueueItemAuthenticator auth : QueueItemAuthenticatorProvider.authenticators()) { Authentication a = auth.authenticate(this); if (a!=null) return a; } - return Tasks.getDefaultAuthenticationOf(task); + return Tasks.getDefaultAuthenticationOf(task, this); } @@ -1519,7 +1684,7 @@ public class Queue extends ResourceController implements Saveable { @Override public String toString() { - return getClass().getName() + ':' + task + ':' + getWhy(); + return getClass().getName() + ':' + task + ':' + id; } /** @@ -1535,24 +1700,31 @@ public class Queue extends ResourceController implements Saveable { /** * Cancels this item, which updates {@link #future} to notify the listener, and * also leaves the queue. + * + * @return true + * if the item was successfully cancelled. */ /*package*/ boolean cancel(Queue q) { boolean r = leave(q); - if (r) future.setAsCancelled(); + if (r) { + future.setAsCancelled(); + LeftItem li = new LeftItem(this); + li.enter(q); + } return r; } } - + /** * An optional interface for actions on Queue.Item. * Lets the action cooperate in queue management. - * + * * @since 1.300-ish. */ public interface QueueAction extends Action { /** - * Returns whether the new item should be scheduled. + * Returns whether the new item should be scheduled. * An action should return true if the associated task is 'different enough' to warrant a separate execution. */ boolean shouldSchedule(List actions); @@ -1564,7 +1736,7 @@ public class Queue extends ResourceController implements Saveable { *

* This handler is consulted every time someone tries to submit a task to the queue. * If any of the registered handlers returns false, the task will not be added - * to the queue, and the task will never get executed. + * to the queue, and the task will never get executed. * *

* The other use case is to add additional {@link Action}s to the task @@ -1581,21 +1753,21 @@ public class Queue extends ResourceController implements Saveable { * upon the start of the build. This list is live, and can be mutated. */ public abstract boolean shouldSchedule(Task p, List actions); - + /** * All registered {@link QueueDecisionHandler}s */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(QueueDecisionHandler.class); + return ExtensionList.lookup(QueueDecisionHandler.class); } } - + /** * {@link Item} in the {@link Queue#waitingList} stage. */ public static final class WaitingItem extends Item implements Comparable { private static final AtomicInteger COUNTER = new AtomicInteger(0); - + /** * This item can be run after this time. */ @@ -1606,7 +1778,7 @@ public class Queue extends ResourceController implements Saveable { super(project, actions, COUNTER.incrementAndGet(), new FutureImpl(project)); this.timestamp = timestamp; } - + public int compareTo(WaitingItem that) { int r = this.timestamp.getTime().compareTo(that.timestamp.getTime()); if (r != 0) return r; @@ -1695,13 +1867,13 @@ public class Queue extends ResourceController implements Saveable { return CauseOfBlockage.fromMessage(Messages._Queue_InProgress()); return CauseOfBlockage.fromMessage(Messages._Queue_BlockedBy(r.getDisplayName())); } - + for (QueueTaskDispatcher d : QueueTaskDispatcher.all()) { CauseOfBlockage cause = d.canRun(this); if (cause != null) return cause; } - + return task.getCauseOfBlockage(); } @@ -1865,7 +2037,7 @@ public class Queue extends ResourceController implements Saveable { * the primary executable (such as {@link AbstractBuild}) that created out of it. */ @Exported - public Executable getExecutable() { + public @CheckForNull Executable getExecutable() { return outcome!=null ? outcome.getPrimaryWorkUnit().getExecutable() : null; } @@ -1986,10 +2158,7 @@ public class Queue extends ResourceController implements Saveable { 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() { @@ -2000,51 +2169,51 @@ public class Queue extends ResourceController implements Saveable { cancel(); } } - + /** * {@link ArrayList} of {@link Item} with more convenience methods. */ 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; } } return null; } - + 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); } } return result; } - + public boolean containsKey(Task task) { return get(task) != null; } - + public T remove(Task task) { Iterator it = iterator(); while (it.hasNext()) { T t = it.next(); - if (t.task == task) { + if (t.task.equals(task)) { it.remove(); return t; } } return null; } - + public void put(Task task, T item) { - assert item.task == task; + assert item.task.equals(task); add(item); } - + public ItemList values() { return this; } 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 * Resources are compared by using their {@link #displayName names}, and - * need not have the "same object" semantics. + * need not have the "same object" semantics. * * @since 1.121 */ @@ -42,7 +46,7 @@ public final class Resource { * Human-readable name of this resource. * Used for rendering HTML. */ - public final String displayName; + public final @Nonnull String displayName; /** * Parent resource. @@ -52,30 +56,30 @@ public final class Resource { * so acquiring the parent resource always imply acquiring all * the child resources. */ - public final Resource parent; + public final @CheckForNull Resource parent; /** * Maximum number of concurrent write. */ public final int numConcurrentWrite; - public Resource(Resource parent, String displayName) { + public Resource(@CheckForNull Resource parent, @Nonnull String displayName) { this(parent,displayName,1); } /** * @since 1.155 */ - public Resource(Resource parent, String displayName, int numConcurrentWrite) { + public Resource(@CheckForNull Resource parent, @Nonnull String displayName, int numConcurrentWrite) { if(numConcurrentWrite<1) throw new IllegalArgumentException(); - + this.parent = parent; this.displayName = displayName; this.numConcurrentWrite = numConcurrentWrite; } - public Resource(String displayName) { + public Resource(@Nonnull String displayName) { this(null,displayName); } diff --git a/core/src/main/java/hudson/model/ResourceActivity.java b/core/src/main/java/hudson/model/ResourceActivity.java index 91092b9308fe66b56934ccc578aca00e6a6ecad3..5199a844f5c9fa9cc32aae5a040c8c8b914565ab 100644 --- a/core/src/main/java/hudson/model/ResourceActivity.java +++ b/core/src/main/java/hudson/model/ResourceActivity.java @@ -39,7 +39,7 @@ public interface ResourceActivity { * *

* 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/ResourceController.java b/core/src/main/java/hudson/model/ResourceController.java index b1d44ecf15045285e1d04781e5b56c3ffdfeacf8..d1239f3eec5147ff76d3ebc0d11f8277ecb59bd4 100644 --- a/core/src/main/java/hudson/model/ResourceController.java +++ b/core/src/main/java/hudson/model/ResourceController.java @@ -30,6 +30,7 @@ import java.util.Collection; import java.util.AbstractCollection; import java.util.Iterator; import java.util.concurrent.CopyOnWriteArraySet; +import javax.annotation.Nonnull; /** * Controls mutual exclusion of {@link ResourceList}. @@ -73,7 +74,7 @@ public class ResourceController { * @throws InterruptedException * the thread can be interrupted while waiting for the available resources. */ - public void execute( Runnable task, ResourceActivity activity ) throws InterruptedException { + public void execute(@Nonnull Runnable task, ResourceActivity activity ) throws InterruptedException { ResourceList resources = activity.getResourceList(); synchronized(this) { while(inUse.isCollidingWith(resources)) diff --git a/core/src/main/java/hudson/model/RestartListener.java b/core/src/main/java/hudson/model/RestartListener.java index 3fc3c72c578045a186a8dd2cbe2461020dc2a96a..524dcfc20eb0552510cdb21ecbf146ba8a3b2c68 100644 --- a/core/src/main/java/hudson/model/RestartListener.java +++ b/core/src/main/java/hudson/model/RestartListener.java @@ -30,7 +30,7 @@ public abstract class RestartListener implements ExtensionPoint { * Returns all the registered {@link LabelFinder}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(RestartListener.class); + return ExtensionList.lookup(RestartListener.class); } /** 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 addc19fa9c0d2a7582f34112a0f15d441940d589..cb42935e4f223f30bdf3e60e4a10e20dae8f0516 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.ExtensionList; 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 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); @@ -376,7 +396,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; } /** @@ -506,6 +521,11 @@ 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 @@ -546,7 +566,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()); @@ -556,8 +576,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); @@ -577,8 +597,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(); @@ -813,8 +848,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(); @@ -849,8 +884,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(); @@ -914,7 +949,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); } @@ -1064,16 +1106,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); @@ -1254,10 +1297,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(); @@ -1423,9 +1480,11 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run.RunExecution exec = RunnerStack.INSTANCE.peek(); + if (exec == null) { + return; + } + exec.checkpoints.report(id); } /** * @see CheckPoint#block() */ - /*package*/ static void waitForCheckpoint(CheckPoint id, @CheckForNull BuildListener listener, @CheckForNull String waiter) throws InterruptedException { + /*package*/ static void waitForCheckpoint(@Nonnull CheckPoint id, @CheckForNull BuildListener listener, @CheckForNull String waiter) throws InterruptedException { while(true) { - Run b = RunnerStack.INSTANCE.peek().getBuild().getPreviousBuildInProgress(); + Run.RunExecution exec = RunnerStack.INSTANCE.peek(); + if (exec == null) { + return; + } + Run b = exec.getBuild().getPreviousBuildInProgress(); if(b==null) return; // no pending earlier build Run.RunExecution runner = b.runner; if(runner==null) { @@ -1521,12 +1592,12 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run getAttributes() { + public @Nonnull Map getAttributes() { return attributes; } } @@ -1622,11 +1693,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())); @@ -1906,10 +1987,14 @@ public abstract class Run ,RunT extends Run,RunT extends Run run, @Nonnull ResultTrend trend); + } + /** * Gets an object which represents the single line summary of the status of this build * (especially in comparison with the previous build.) + * @see StatusSummarizer */ - public Summary getBuildStatusSummary() { + public @Nonnull Summary getBuildStatusSummary() { if (isBuilding()) { return new Summary(false, Messages.Run_Summary_Unknown()); } ResultTrend trend = ResultTrend.getResultTrend(this); + for (StatusSummarizer summarizer : ExtensionList.lookup(StatusSummarizer.class)) { + Summary summary = summarizer.summarize(this, trend); + if (summary != null) { + return summary; + } + } + switch (trend) { case ABORTED : return new Summary(false, Messages.Run_Summary_Aborted()); @@ -1949,11 +2056,10 @@ public abstract class Run ,RunT extends Run,RunT extends Run0) - return new Summary(worseOverride != null ? worseOverride : true, - Messages.Run_Summary_TestFailures(trN.getFailCount())); - } else { - if(trN!=null && trN.getFailCount()!= 0) { - if(trP.getFailCount()==0) - return new Summary(worseOverride != null ? worseOverride : true, - Messages.Run_Summary_TestsStartedToFail(trN.getFailCount())); - if(trP.getFailCount() < trN.getFailCount()) - return new Summary(worseOverride != null ? worseOverride : true, - Messages.Run_Summary_MoreTestsFailing(trN.getFailCount()-trP.getFailCount(), trN.getFailCount())); - if(trP.getFailCount() > trN.getFailCount()) - return new Summary(worseOverride != null ? worseOverride : false, - Messages.Run_Summary_LessTestsFailing(trP.getFailCount()-trN.getFailCount(), trN.getFailCount())); - - return new Summary(worseOverride != null ? worseOverride : false, - Messages.Run_Summary_TestsStillFailing(trN.getFailCount())); - } - } - } - - return new Summary(worseOverride != null ? worseOverride : false, - Messages.Run_Summary_Unstable()); - } - /** * Serves the artifacts. + * @throws AccessDeniedException Access denied */ - public DirectoryBrowserSupport doArtifact() { + public @Nonnull DirectoryBrowserSupport doArtifact() { if(Functions.isArtifactsPermissionEnabled()) { checkPermission(ARTIFACTS); } - return new DirectoryBrowserSupport(this, getArtifactManager().root(), project.getDisplayName() + ' ' + getDisplayName(), "package.png", true); + return new DirectoryBrowserSupport(this, getArtifactManager().root(), Messages.Run_ArtifactsBrowserTitle(project.getDisplayName(), getDisplayName()), "package.png", true); } /** @@ -2051,8 +2123,9 @@ public abstract class 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) { @@ -2161,6 +2235,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. * @@ -2177,10 +2252,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(); @@ -2198,7 +2273,7 @@ public abstract class Run ,RunT extends Run,RunT extends Run fromExternalizableId(String id) { + /** + * Tries to find a run from an persisted identifier. + * @param id as produced by {@link #getExternalizableId} + * @return the same run, or null if the job or run was not found + * @throws IllegalArgumentException if the ID is malformed + */ + public @CheckForNull static Run fromExternalizableId(String id) throws IllegalArgumentException { int hash = id.lastIndexOf('#'); if (hash <= 0) { throw new IllegalArgumentException("Invalid id"); } String jobName = id.substring(0, hash); - int number = Integer.parseInt(id.substring(hash + 1)); - - Job job = Jenkins.getInstance().getItemByFullName(jobName, Job.class); + int number; + try { + number = Integer.parseInt(id.substring(hash + 1)); + } catch (NumberFormatException x) { + throw new IllegalArgumentException(x); + } + Jenkins j = Jenkins.getInstance(); + if (j == null) { + return null; + } + Job job = j.getItemByFullName(jobName, Job.class); if (job == null) { - throw new IllegalArgumentException("no such job " + jobName); + return null; } return job.getBuildByNumber(number); } @@ -2239,7 +2333,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; @@ -2310,10 +2402,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; } }; @@ -224,8 +217,9 @@ public final class RunMap> extends AbstractLazyLoadRunMap try { R b = cons.create(d); b.onLoad(); - if (LOGGER.isLoggable(FINEST)) - LOGGER.log(FINEST,"Loaded " + b.getFullDisplayName(),new ThisIsHowItsLoaded()); + if (LOGGER.isLoggable(FINEST)) { + LOGGER.log(FINEST, "Loaded " + b.getFullDisplayName() + " in " + Thread.currentThread().getName(), new ThisIsHowItsLoaded()); + } return b; } catch (Run.InvalidDirectoryNameException x) { Level lvl; 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/RunnerStack.java b/core/src/main/java/hudson/model/RunnerStack.java index 7fd8ea761dc33188e8c000113d9e946bd743ded1..2cade6d5efb6e9cf0958cc1ddbb14ef36fadfa9d 100644 --- a/core/src/main/java/hudson/model/RunnerStack.java +++ b/core/src/main/java/hudson/model/RunnerStack.java @@ -28,6 +28,7 @@ import hudson.model.Run.RunExecution; import java.util.Stack; import java.util.Map; import java.util.WeakHashMap; +import javax.annotation.CheckForNull; /** * Keeps track of {@link RunExecution}s that are currently executing on the given thread @@ -53,8 +54,23 @@ final class RunnerStack { if(s.isEmpty()) stack.remove(e); } - synchronized RunExecution peek() { - return stack.get(Executor.currentExecutor()).peek(); + /** + * Looks up the currently running build, if known. + *

While most {@link Run} implementations do add a {@link RunExecution} to the stack for the duration of the build, + * those which have a different implementation of {@link Run#run} (or which do additional work after {@link Run#execute} completes) + * may not consistently or ever keep an execution on the stack. + * In such cases this method will return null, meaning that {@link CheckPoint#block(BuildListener, String)} and {@link CheckPoint#report} will do nothing. + * @return a running build, or null if one has not been recorded + */ + synchronized @CheckForNull RunExecution peek() { + Executor e = Executor.currentExecutor(); + if (e != null) { + Stack s = stack.get(e); + if (s != null && !s.isEmpty()) { + return s.peek(); + } + } + return null; } static final RunnerStack INSTANCE = new RunnerStack(); 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..92ac16518d1b07f29ca8d8a86d13c86aa7f67f79 100644 --- a/core/src/main/java/hudson/model/Slave.java +++ b/core/src/main/java/hudson/model/Slave.java @@ -56,12 +56,13 @@ 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.security.MasterToSlaveCallable; import jenkins.slaves.WorkspaceLocator; import org.apache.commons.io.IOUtils; -import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; @@ -133,7 +134,6 @@ public abstract class Slave extends Node implements Serializable { */ private String userId; - @DataBoundConstructor public Slave(String name, String nodeDescription, String remoteFS, String numExecutors, Mode mode, String labelString, ComputerLauncher launcher, RetentionStrategy retentionStrategy, List> nodeProperties) throws FormException, IOException { this(name,nodeDescription,remoteFS,Util.tryParseNumber(numExecutors, 1).intValue(),mode,labelString,launcher,retentionStrategy, nodeProperties); @@ -148,7 +148,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; @@ -209,6 +209,10 @@ public abstract class Slave extends Node implements Serializable { return name; } + @Override public String toString() { + return getClass().getName() + "[" + name + "]"; + } + public void setNodeName(String name) { this.name = name; } @@ -284,7 +288,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 +346,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); + } } /** @@ -440,7 +456,7 @@ public abstract class Slave extends Node implements Serializable { *

  • When it's read on this side as a return value, it morphs itself into {@link ClockDifference}. * */ - private static final class GetClockDifference1 implements Callable { + private static final class GetClockDifference1 extends MasterToSlaveCallable { public ClockDifference call() { // this method must be being invoked locally, which means the clock is in sync return new ClockDifference(0); @@ -453,7 +469,7 @@ public abstract class Slave extends Node implements Serializable { private static final long serialVersionUID = 1L; } - private static final class GetClockDifference2 implements Callable { + private static final class GetClockDifference2 extends MasterToSlaveCallable { /** * Capture the time on the master when this object is sent to remote, which is when * {@link GetClockDifference1#writeReplace()} is run. 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/TaskAction.java b/core/src/main/java/hudson/model/TaskAction.java index 5785fc50f8a3888661ccb1c6b88284b34551f84c..f42c7fdae612d12c8ee8d3a42c1082553da4e996 100644 --- a/core/src/main/java/hudson/model/TaskAction.java +++ b/core/src/main/java/hudson/model/TaskAction.java @@ -60,15 +60,22 @@ public abstract class TaskAction extends AbstractModelObject implements Action { protected transient WeakReference log; /** - * Gets the permission object that represents the permission to perform this task. + * Gets the permission object that represents the permission (against {@link #getACL}) to perform this task. + * Generally your implementation of {@link #getIconFileName} should return null if {@code !getACL().hasPermission(getPermission())}. */ protected abstract Permission getPermission(); /** - * Gets the {@link ACL} against which the permissions are checked. + * Gets the {@link ACL} against which {@link #getPermission} is checked. */ protected abstract ACL getACL(); + /** + * @inheritDoc + * @see #getPermission + */ + @Override public abstract String getIconFileName(); + /** * @deprecated as of 1.350 * Use {@link #obtainLog()}, which returns the same object in a more type-safe signature. 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/TopLevelItemDescriptor.java b/core/src/main/java/hudson/model/TopLevelItemDescriptor.java index ec8a2aab9c157025afd609b5a93acf38a7bfaca7..f4d5a165b96ec4ad244445e3f53f78e7c3c0db78 100644 --- a/core/src/main/java/hudson/model/TopLevelItemDescriptor.java +++ b/core/src/main/java/hudson/model/TopLevelItemDescriptor.java @@ -69,6 +69,7 @@ public abstract class TopLevelItemDescriptor extends Descriptor { * {@link TopLevelItemDescriptor}s that act like a wizard and produces different * object types than {@link #clazz} can override this method to augment * instance-descriptor relationship. + * @since 1.410 */ public boolean testInstance(TopLevelItem i) { return clazz.isInstance(i); diff --git a/core/src/main/java/hudson/model/TransientBuildActionFactory.java b/core/src/main/java/hudson/model/TransientBuildActionFactory.java index ec3adf99badf048120e90df56f51c078f7b0559b..314590f0ecc092e9d7e9283f441dd7074d22b2d0 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. @@ -43,6 +45,6 @@ public abstract class TransientBuildActionFactory implements ExtensionPoint { * Returns all the registered {@link TransientBuildActionFactory}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(TransientBuildActionFactory.class); + return ExtensionList.lookup(TransientBuildActionFactory.class); } } \ No newline at end of file diff --git a/core/src/main/java/hudson/model/TransientComputerActionFactory.java b/core/src/main/java/hudson/model/TransientComputerActionFactory.java index ee724ce044a8f440a08fb7e02b411475224fd54f..37fc369e95f51612f2939dd69b21ca0bc237a4e1 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 { /** @@ -55,7 +57,7 @@ public abstract class TransientComputerActionFactory implements ExtensionPoint { * Returns all the registered {@link TransientComputerActionFactory}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(TransientComputerActionFactory.class); + return ExtensionList.lookup(TransientComputerActionFactory.class); } diff --git a/core/src/main/java/hudson/model/TransientProjectActionFactory.java b/core/src/main/java/hudson/model/TransientProjectActionFactory.java index 9588d83fe9a7c842570685649c55f2c856ea1f94..35acbaebb9a7b5cda30ac662a1d3d4888da8566b 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 { /** @@ -66,6 +68,6 @@ public abstract class TransientProjectActionFactory implements ExtensionPoint { * Returns all the registered {@link TransientProjectActionFactory}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(TransientProjectActionFactory.class); + return ExtensionList.lookup(TransientProjectActionFactory.class); } } diff --git a/core/src/main/java/hudson/model/TransientUserActionFactory.java b/core/src/main/java/hudson/model/TransientUserActionFactory.java index 815df7c9b26019401e62d238bb12193da3d28f92..d45aebdfbf449422ad560c7725918adef6d7d316 100644 --- a/core/src/main/java/hudson/model/TransientUserActionFactory.java +++ b/core/src/main/java/hudson/model/TransientUserActionFactory.java @@ -55,6 +55,6 @@ public abstract class TransientUserActionFactory implements ExtensionPoint { * Returns all the registered {@link TransientUserActionFactory}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(TransientUserActionFactory.class); + return ExtensionList.lookup(TransientUserActionFactory.class); } } \ No newline at end of file diff --git a/core/src/main/java/hudson/model/TransientViewActionFactory.java b/core/src/main/java/hudson/model/TransientViewActionFactory.java index 05c9122102f1c32bc87b962b1a9ac62c910f4d1c..43633bac2d84154343dcea184e6ea2cce2d2d10e 100644 --- a/core/src/main/java/hudson/model/TransientViewActionFactory.java +++ b/core/src/main/java/hudson/model/TransientViewActionFactory.java @@ -25,7 +25,7 @@ public abstract class TransientViewActionFactory implements ExtensionPoint { * Returns all the registered {@link TransientViewActionFactory}s. */ public static ExtensionList all() { - return Jenkins.getInstance().getExtensionList(TransientViewActionFactory.class); + return ExtensionList.lookup(TransientViewActionFactory.class); } /** 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 ba0bd6efd5930ccd61d356f4fec754643a17e9e7..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()); diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java index 470ecaccabf633eca89a8393ee27b563b4f27e96..77b5926c4b7d38ba3b7a7775aed46fe6c2fd510c 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); } @@ -230,7 +218,16 @@ public class UpdateSite { * Verifies the signature in the update center data file. */ private FormValidation verifySignature(JSONObject o) throws IOException { - return new JSONSignatureValidator("update site '"+id+"'").verifySignature(o); + return getJsonSignatureValidator().verifySignature(o); + } + + /** + * Let sub-classes of UpdateSite provide their own signature validator. + * @return the signature validator. + */ + @Nonnull + protected JSONSignatureValidator getJsonSignatureValidator() { + return new JSONSignatureValidator("update site '"+id+"'"); } /** @@ -637,8 +634,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 f350b06ee99140d272fb528f336d42871a523a47..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; @@ -156,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) { @@ -172,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 94b6369975363bb135d3572006ae37062ea29146..c4190ee4af4386ccfe7656ed819118c30c2cf2de 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,15 +71,17 @@ 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; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Represents a user. @@ -121,8 +129,28 @@ 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() { + Jenkins j = Jenkins.getInstance(); + if (j == null) { + return IdStrategy.CASE_INSENSITIVE; + } + SecurityRealm realm = j.getSecurityRealm(); + if (realm == null) { + return IdStrategy.CASE_INSENSITIVE; + } + return realm.getUserIdStrategy(); + } + public int compareTo(User that) { - return this.id.compareTo(that.id); + return idStrategy().compare(this.id, that.id); } /** @@ -164,36 +192,34 @@ public class User extends AbstractModelObject implements AccessControlled, Descr return id; } - public String getUrl() { - return "user/"+Util.rawEncode(id); + public @Nonnull String getUrl() { + return "user/"+Util.rawEncode(idStrategy().keyFor(id)); } - public String getSearchUrl() { - return "/user/"+Util.rawEncode(id); + public @Nonnull String getSearchUrl() { + return "/user/"+Util.rawEncode(idStrategy().keyFor(id)); } /** * The URL of the user page. */ @Exported(visibility=999) - public String getAbsoluteUrl() { + public @Nonnull String getAbsoluteUrl() { return Jenkins.getInstance().getRootUrl()+getUrl(); } /** * Gets the human readable name of this user. * This is configurable by the user. - * - * @return - * never null. */ @Exported(visibility=999) - public String getFullName() { + public @Nonnull String getFullName() { return fullName; } /** - * Sets the human readable name of thie user. + * Sets the human readable name of the user. + * If the input parameter is empty, the user's ID will be set. */ public void setFullName(String name) { if(Util.fixEmptyAndTrim(name)==null) name=id; @@ -201,7 +227,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr } @Exported - public String getDescription() { + public @CheckForNull String getDescription() { return description; } @@ -215,7 +241,7 @@ public class User extends AbstractModelObject implements AccessControlled, Descr /** * Updates the user object by adding a property. */ - public synchronized void addProperty(UserProperty p) throws IOException { + public synchronized void addProperty(@Nonnull UserProperty p) throws IOException { UserProperty old = getProperty(p.getClass()); List ps = new ArrayList(properties); if(old!=null) @@ -247,19 +273,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 @Nonnull 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}); } @@ -293,9 +331,11 @@ public class User extends AbstractModelObject implements AccessControlled, Descr * (by creating a new {@link User} object if none exists.) * If false, this method will return null if {@link User} object * with the given name doesn't exist. + * @return Requested user. May be {@code null} if a user does not exist and + * {@code create} is false. * @deprecated use {@link User#get(String, boolean, java.util.Map)} */ - public static User get(String idOrFullName, boolean create) { + public static @Nullable User get(String idOrFullName, boolean create) { return get(idOrFullName, create, Collections.emptyMap()); } @@ -311,14 +351,18 @@ public class User extends AbstractModelObject implements AccessControlled, Descr * @param context * contextual environment this user idOfFullName was retrieved from, * that can help resolve the user ID + * + * @return + * An existing or created user. May be {@code null} if a user does not exist and + * {@code create} is false. */ - public static User get(String idOrFullName, boolean create, Map context) { + public static @Nullable User get(String idOrFullName, boolean create, Map context) { if(idOrFullName==null) return null; // sort resolvers by priority - List resolvers = new ArrayList(Jenkins.getInstance().getExtensionList(CanonicalIdResolver.class)); + List resolvers = new ArrayList(ExtensionList.lookup(CanonicalIdResolver.class)); Collections.sort(resolvers); String id = null; @@ -330,28 +374,78 @@ public class User extends AbstractModelObject implements AccessControlled, Descr } } // DefaultUserCanonicalIdResolver will always return a non-null id if all other CanonicalIdResolver failed - + if (id == null) { + throw new IllegalStateException("The user id should be always non-null thanks to DefaultUserCanonicalIdResolver"); + } return getOrCreate(id, idOrFullName, create); } /** - * retrieve a user by its ID, and create a new one if requested + * Retrieve a user by its ID, and create a new one if requested. + * @return + * An existing or created user. May be {@code null} if a user does not exist and + * {@code create} is false. */ - private static User getOrCreate(String id, String fullName, boolean create) { - String idkey = id.toLowerCase(Locale.ENGLISH); + private static @Nullable User getOrCreate(@Nonnull String id, @Nonnull String fullName, boolean create) { + String idkey = idStrategy().keyFor(id); - User u = byName.get(idkey); - if (u==null && (create || getConfigFileFor(id).exists())) { + byNameLock.readLock().lock(); + User u; + try { + u = byName.get(idkey); + } finally { + byNameLock.readLock().unlock(); + } + final File configFile = getConfigFileFor(id); + if (!configFile.isFile() && !configFile.getParentFile().isDirectory()) { + // check for legacy users and migrate if safe to do so. + File[] legacy = getLegacyConfigFilesFor(id); + if (legacy != null && legacy.length > 0) { + for (File legacyUserDir : legacy) { + final XmlFile legacyXml = new XmlFile(XSTREAM, new File(legacyUserDir, "config.xml")); + try { + Object o = legacyXml.read(); + if (o instanceof User) { + if (idStrategy().equals(id, legacyUserDir.getName()) && !idStrategy().filenameOf(legacyUserDir.getName()) + .equals(legacyUserDir.getName())) { + if (!legacyUserDir.renameTo(configFile.getParentFile())) { + LOGGER.log(Level.WARNING, "Failed to migrate user record from {0} to {1}", + new Object[]{legacyUserDir, configFile.getParentFile()}); + } + break; + } + } else { + LOGGER.log(Level.FINE, "Unexpected object loaded from {0}: {1}", + new Object[]{ legacyUserDir, o }); + } + } catch (IOException e) { + LOGGER.log(Level.FINE, String.format("Exception trying to load user from {0}: {1}", + new Object[]{ legacyUserDir, e.getMessage() }), e); + } + } + } + } + if (u==null && (create || configFile.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()); } - } - if (LOGGER.isLoggable(Level.FINE) && id.equals(fullName) && fullName.matches(".+ _\\S+@\\S+_")) { - LOGGER.log(Level.FINE, "[JENKINS-16332] Suspicious fullName being stored: " + fullName, new Throwable()); + } else if (!id.equals(fullName) && !configFile.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; @@ -373,7 +467,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; @@ -381,7 +478,8 @@ public class User extends AbstractModelObject implements AccessControlled, Descr /** * Gets all the users. */ - public static Collection getAll() { + public static @Nonnull Collection getAll() { + final IdStrategy strategy = idStrategy(); if(System.currentTimeMillis() -lastScanned>10000) { // occasionally scan the file system to check new users // whether we should do this only once at start up or not is debatable. @@ -394,17 +492,24 @@ public class User extends AbstractModelObject implements AccessControlled, Descr for (File subdir : subdirs) if(new File(subdir,"config.xml").exists()) { - String name = subdir.getName(); + String name = strategy.idFromFilename(subdir.getName()); User.getOrCreate(name, name, true); } 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() { + public int compare(User o1, User o2) { - return o1.getId().compareToIgnoreCase(o2.getId()); + return strategy.compare(o1.getId(), o2.getId()); } }); return r; @@ -414,55 +519,90 @@ 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(); + } } /** * Returns the user name. */ - public String getDisplayName() { + public @Nonnull String getDisplayName() { return getFullName(); } + /** true if {@link AbstractBuild#hasParticipant} or {@link hudson.model.Cause.UserIdCause} */ + private boolean relatedTo(@Nonnull 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); - } - } + public @Nonnull RunList getBuilds() { + 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); + }); } /** * Gets all the {@link AbstractProject}s that this user has committed to. * @since 1.191 */ - public Set> getProjects() { + public @Nonnull Set> getProjects() { Set> r = new HashSet>(); for (AbstractProject p : Jenkins.getInstance().getAllItems(AbstractProject.class)) if(p.hasParticipant(this)) @@ -482,7 +622,17 @@ 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"); + } + + private static final File[] getLegacyConfigFilesFor(final String id) { + return getRootDir().listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isDirectory() && new File(pathname, "config.xml").isFile() && idStrategy().equals( + pathname.getName(), id); + } + }); } /** @@ -508,8 +658,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))); } /** @@ -526,11 +682,11 @@ public class User extends AbstractModelObject implements AccessControlled, Descr public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException { checkPermission(Jenkins.ADMINISTER); - fullName = req.getParameter("fullName"); - description = req.getParameter("description"); - JSONObject json = req.getSubmittedForm(); + fullName = json.getString("fullName"); + description = json.getString("description"); + List props = new ArrayList(); int i = 0; for (UserPropertyDescriptor d : UserProperty.all()) { @@ -562,7 +718,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; } @@ -573,21 +729,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; } @@ -605,10 +758,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. */ @@ -625,7 +787,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); } }; @@ -643,8 +805,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(); } /** @@ -659,15 +822,23 @@ public class User extends AbstractModelObject implements AccessControlled, Descr return Collections.emptyList(); } List r = new ArrayList(); - for (GrantedAuthority a : impersonate().getAuthorities()) { + Authentication authentication; + try { + authentication = impersonate(); + } catch (UsernameNotFoundException x) { + LOGGER.log(Level.FINE, "cannot look up authorities for " + id, x); + return Collections.emptyList(); + } + for (GrantedAuthority a : authentication.getAuthorities()) { if (a.equals(SecurityRealm.AUTHENTICATED_AUTHORITY)) { 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; } @@ -767,5 +938,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 266e61acf25dd78a1369086f98d756e6cde90bb2..d03727af6cf4a31a9743576756e479947c3776d1 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; @@ -114,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

    *