From 70a1850e5e29e7a06f43132cf0c4cea51744d0d0 Mon Sep 17 00:00:00 2001 From: Eric Hartmann Date: Mon, 8 Sep 2014 10:38:53 +0200 Subject: [maven-release-plugin] prepare release 4.5-RC1 --- sonar-application/pom.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'sonar-application/pom.xml') diff --git a/sonar-application/pom.xml b/sonar-application/pom.xml index a7e45403831..c04ec45d625 100644 --- a/sonar-application/pom.xml +++ b/sonar-application/pom.xml @@ -1,12 +1,11 @@ - + 4.0.0 org.codehaus.sonar sonar - 4.5-SNAPSHOT + 4.5-RC1 sonar-application @@ -237,8 +236,8 @@ package - - + + -- cgit v1.2.3 From 85da787c6f0f5c47ec6e19681ed00fa8661f0fb8 Mon Sep 17 00:00:00 2001 From: Eric Hartmann Date: Mon, 8 Sep 2014 10:38:54 +0200 Subject: [maven-release-plugin] prepare for next development iteration --- plugins/sonar-core-plugin/pom.xml | 2 +- plugins/sonar-cpd-plugin/pom.xml | 2 +- plugins/sonar-dbcleaner-plugin/pom.xml | 2 +- plugins/sonar-design-plugin/pom.xml | 2 +- plugins/sonar-email-notifications-plugin/pom.xml | 2 +- plugins/sonar-l10n-en-plugin/pom.xml | 2 +- plugins/sonar-xoo-plugin/pom.xml | 2 +- pom.xml | 4 ++-- server/pom.xml | 2 +- server/process/pom.xml | 2 +- server/process/sonar-dummy-app/pom.xml | 2 +- server/process/sonar-process/pom.xml | 2 +- server/sonar-search/pom.xml | 2 +- server/sonar-server/pom.xml | 2 +- server/sonar-web/pom.xml | 2 +- server/sonar-ws-client/pom.xml | 2 +- sonar-application/pom.xml | 2 +- sonar-batch-maven-compat/pom.xml | 2 +- sonar-batch-protocol/pom.xml | 2 +- sonar-batch/pom.xml | 2 +- sonar-check-api/pom.xml | 2 +- sonar-colorizer/pom.xml | 2 +- sonar-core/pom.xml | 2 +- sonar-deprecated/pom.xml | 2 +- sonar-duplications/pom.xml | 2 +- sonar-graph/pom.xml | 2 +- sonar-home/pom.xml | 2 +- sonar-java-api/pom.xml | 2 +- sonar-markdown/pom.xml | 2 +- sonar-maven-plugin/pom.xml | 2 +- sonar-maven3-plugin/pom.xml | 2 +- sonar-plugin-api/pom.xml | 2 +- sonar-testing-harness/pom.xml | 2 +- 33 files changed, 34 insertions(+), 34 deletions(-) (limited to 'sonar-application/pom.xml') diff --git a/plugins/sonar-core-plugin/pom.xml b/plugins/sonar-core-plugin/pom.xml index cde3b3d813f..1ae26fd5ca7 100644 --- a/plugins/sonar-core-plugin/pom.xml +++ b/plugins/sonar-core-plugin/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT ../.. org.codehaus.sonar.plugins diff --git a/plugins/sonar-cpd-plugin/pom.xml b/plugins/sonar-cpd-plugin/pom.xml index 830ba98eb1a..334122c4b91 100644 --- a/plugins/sonar-cpd-plugin/pom.xml +++ b/plugins/sonar-cpd-plugin/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT ../.. org.codehaus.sonar.plugins diff --git a/plugins/sonar-dbcleaner-plugin/pom.xml b/plugins/sonar-dbcleaner-plugin/pom.xml index 7221a1554ae..89878478823 100644 --- a/plugins/sonar-dbcleaner-plugin/pom.xml +++ b/plugins/sonar-dbcleaner-plugin/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT ../.. org.codehaus.sonar.plugins diff --git a/plugins/sonar-design-plugin/pom.xml b/plugins/sonar-design-plugin/pom.xml index 23d29b72808..b2029087b50 100644 --- a/plugins/sonar-design-plugin/pom.xml +++ b/plugins/sonar-design-plugin/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT ../.. org.codehaus.sonar.plugins diff --git a/plugins/sonar-email-notifications-plugin/pom.xml b/plugins/sonar-email-notifications-plugin/pom.xml index 58a18c43dc1..8bb88b08971 100644 --- a/plugins/sonar-email-notifications-plugin/pom.xml +++ b/plugins/sonar-email-notifications-plugin/pom.xml @@ -5,7 +5,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT ../.. diff --git a/plugins/sonar-l10n-en-plugin/pom.xml b/plugins/sonar-l10n-en-plugin/pom.xml index fc7bc2e393d..98585e5b11b 100644 --- a/plugins/sonar-l10n-en-plugin/pom.xml +++ b/plugins/sonar-l10n-en-plugin/pom.xml @@ -25,7 +25,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT ../.. diff --git a/plugins/sonar-xoo-plugin/pom.xml b/plugins/sonar-xoo-plugin/pom.xml index 03c782ce735..6c1a1182306 100644 --- a/plugins/sonar-xoo-plugin/pom.xml +++ b/plugins/sonar-xoo-plugin/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT ../.. org.codehaus.sonar.plugins diff --git a/pom.xml b/pom.xml index d5da7821c11..ff5ea0aeb3a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.codehaus.sonar sonar pom - 4.5-RC1 + 4.5-SNAPSHOT SonarQube http://www.sonarqube.org/ Open source platform for continuous inspection of code quality @@ -1145,7 +1145,7 @@ scm:git:git@github.com:SonarSource/sonarqube.git scm:git:git@github.com:SonarSource/sonarqube.git https://github.com/SonarSource/sonarqube - 4.5-RC1 + HEAD diff --git a/server/pom.xml b/server/pom.xml index 9388a2a026c..c0ec6370c42 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT server pom diff --git a/server/process/pom.xml b/server/process/pom.xml index 08edeba49b9..5bd42b26e46 100644 --- a/server/process/pom.xml +++ b/server/process/pom.xml @@ -3,7 +3,7 @@ org.codehaus.sonar server - 4.5-RC1 + 4.5-SNAPSHOT ../ 4.0.0 diff --git a/server/process/sonar-dummy-app/pom.xml b/server/process/sonar-dummy-app/pom.xml index f32e6d9ef16..606087bf511 100644 --- a/server/process/sonar-dummy-app/pom.xml +++ b/server/process/sonar-dummy-app/pom.xml @@ -3,7 +3,7 @@ org.codehaus.sonar process - 4.5-RC1 + 4.5-SNAPSHOT ../ 4.0.0 diff --git a/server/process/sonar-process/pom.xml b/server/process/sonar-process/pom.xml index 7897d26b5a4..a0e76e6c864 100644 --- a/server/process/sonar-process/pom.xml +++ b/server/process/sonar-process/pom.xml @@ -3,7 +3,7 @@ org.codehaus.sonar process - 4.5-RC1 + 4.5-SNAPSHOT ../ 4.0.0 diff --git a/server/sonar-search/pom.xml b/server/sonar-search/pom.xml index 88e420dff1d..cb462c0a7cb 100644 --- a/server/sonar-search/pom.xml +++ b/server/sonar-search/pom.xml @@ -3,7 +3,7 @@ org.codehaus.sonar server - 4.5-RC1 + 4.5-SNAPSHOT ../ 4.0.0 diff --git a/server/sonar-server/pom.xml b/server/sonar-server/pom.xml index 0484e17e54c..d39abad9b68 100644 --- a/server/sonar-server/pom.xml +++ b/server/sonar-server/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar server - 4.5-RC1 + 4.5-SNAPSHOT .. sonar-server diff --git a/server/sonar-web/pom.xml b/server/sonar-web/pom.xml index 2c0e9bba46e..98ecf83d8d9 100644 --- a/server/sonar-web/pom.xml +++ b/server/sonar-web/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar server - 4.5-RC1 + 4.5-SNAPSHOT .. sonar-web diff --git a/server/sonar-ws-client/pom.xml b/server/sonar-ws-client/pom.xml index 1bdfc540aa5..ac59f2f4ab7 100644 --- a/server/sonar-ws-client/pom.xml +++ b/server/sonar-ws-client/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar server - 4.5-RC1 + 4.5-SNAPSHOT .. sonar-ws-client diff --git a/sonar-application/pom.xml b/sonar-application/pom.xml index c04ec45d625..c5f1d9721f3 100644 --- a/sonar-application/pom.xml +++ b/sonar-application/pom.xml @@ -5,7 +5,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-application diff --git a/sonar-batch-maven-compat/pom.xml b/sonar-batch-maven-compat/pom.xml index e087a36c454..28bf19e362c 100644 --- a/sonar-batch-maven-compat/pom.xml +++ b/sonar-batch-maven-compat/pom.xml @@ -5,7 +5,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-batch-maven-compat diff --git a/sonar-batch-protocol/pom.xml b/sonar-batch-protocol/pom.xml index 9cfea27cb56..35cc8af069a 100644 --- a/sonar-batch-protocol/pom.xml +++ b/sonar-batch-protocol/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-batch-protocol diff --git a/sonar-batch/pom.xml b/sonar-batch/pom.xml index f8035e48ad0..89f7c9252a4 100644 --- a/sonar-batch/pom.xml +++ b/sonar-batch/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-batch diff --git a/sonar-check-api/pom.xml b/sonar-check-api/pom.xml index 7162b8538ca..bd4450a13ca 100644 --- a/sonar-check-api/pom.xml +++ b/sonar-check-api/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT .. sonar-check-api diff --git a/sonar-colorizer/pom.xml b/sonar-colorizer/pom.xml index 7b4f6abf557..5f0d84f1032 100644 --- a/sonar-colorizer/pom.xml +++ b/sonar-colorizer/pom.xml @@ -5,7 +5,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT .. diff --git a/sonar-core/pom.xml b/sonar-core/pom.xml index 3cee5c6c3d4..bad0f4f5503 100644 --- a/sonar-core/pom.xml +++ b/sonar-core/pom.xml @@ -5,7 +5,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-core diff --git a/sonar-deprecated/pom.xml b/sonar-deprecated/pom.xml index d607cbe626f..84a01b6bf4e 100644 --- a/sonar-deprecated/pom.xml +++ b/sonar-deprecated/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-deprecated SonarQube :: Deprecated diff --git a/sonar-duplications/pom.xml b/sonar-duplications/pom.xml index d67cb93213b..9808d5de697 100644 --- a/sonar-duplications/pom.xml +++ b/sonar-duplications/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-duplications diff --git a/sonar-graph/pom.xml b/sonar-graph/pom.xml index 56cb0467077..76e711774ee 100644 --- a/sonar-graph/pom.xml +++ b/sonar-graph/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT .. org.codehaus.sonar diff --git a/sonar-home/pom.xml b/sonar-home/pom.xml index fdc8271460a..83edfd67fd7 100644 --- a/sonar-home/pom.xml +++ b/sonar-home/pom.xml @@ -5,7 +5,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-home diff --git a/sonar-java-api/pom.xml b/sonar-java-api/pom.xml index d8cee42d9c4..a87bbb2afdd 100644 --- a/sonar-java-api/pom.xml +++ b/sonar-java-api/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-java-api SonarQube :: Java API diff --git a/sonar-markdown/pom.xml b/sonar-markdown/pom.xml index 6ba4f0bb61c..e2ef82f8633 100644 --- a/sonar-markdown/pom.xml +++ b/sonar-markdown/pom.xml @@ -5,7 +5,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT .. diff --git a/sonar-maven-plugin/pom.xml b/sonar-maven-plugin/pom.xml index d6d3945afdb..0a07a082954 100644 --- a/sonar-maven-plugin/pom.xml +++ b/sonar-maven-plugin/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-maven-plugin maven-plugin diff --git a/sonar-maven3-plugin/pom.xml b/sonar-maven3-plugin/pom.xml index 49d43f42376..a2df4a45464 100644 --- a/sonar-maven3-plugin/pom.xml +++ b/sonar-maven3-plugin/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-maven3-plugin pom diff --git a/sonar-plugin-api/pom.xml b/sonar-plugin-api/pom.xml index 58d20b13d08..46985b00c84 100644 --- a/sonar-plugin-api/pom.xml +++ b/sonar-plugin-api/pom.xml @@ -5,7 +5,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-plugin-api diff --git a/sonar-testing-harness/pom.xml b/sonar-testing-harness/pom.xml index 94a7466b88e..f1814ab99f5 100644 --- a/sonar-testing-harness/pom.xml +++ b/sonar-testing-harness/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar sonar - 4.5-RC1 + 4.5-SNAPSHOT sonar-testing-harness jar -- cgit v1.2.3 From aeee283d2109c425828c96bc2952b947cce56566 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Fri, 12 Sep 2014 10:19:06 +0200 Subject: SONAR-4898 refactor process monitoring --- server/pom.xml | 3 +- server/process/sonar-process/pom.xml | 21 +- .../main/java/org/sonar/process/ProcessUtils.java | 36 +- .../test/java/org/sonar/process2/MonitorTest.java | 23 ++ server/sonar-process-monitor/pom.xml | 87 ++++ .../org/sonar/process/monitor/JavaCommand.java | 175 ++++++++ .../sonar/process/monitor/JavaProcessLauncher.java | 124 ++++++ .../org/sonar/process/monitor/JmxConnector.java | 35 ++ .../java/org/sonar/process/monitor/Monitor.java | 208 ++++++++++ .../org/sonar/process/monitor/PingerThread.java | 60 +++ .../java/org/sonar/process/monitor/ProcessRef.java | 98 +++++ .../org/sonar/process/monitor/RmiJmxConnector.java | 133 +++++++ .../org/sonar/process/monitor/StreamGobbler.java | 69 ++++ .../sonar/process/monitor/TerminatorThread.java | 78 ++++ .../java/org/sonar/process/monitor/Timeouts.java | 103 +++++ .../org/sonar/process/monitor/WatcherThread.java | 70 ++++ .../org/sonar/process/monitor/package-info.java | 23 ++ .../process/monitor/CallVerifierJmxConnector.java | 38 ++ .../monitor/ImpossibleToConnectJmxConnector.java | 42 ++ .../monitor/InfiniteTerminationRmiConnector.java | 38 ++ .../org/sonar/process/monitor/JavaCommandTest.java | 79 ++++ .../process/monitor/JavaProcessLauncherTest.java | 43 ++ .../org/sonar/process/monitor/MonitorTest.java | 442 +++++++++++++++++++++ .../monitor/TerminationFailureRmiConnector.java | 31 ++ .../org/sonar/process/monitor/TimeoutsTest.java | 53 +++ .../sonar/process/monitor/WatcherThreadTest.java | 52 +++ .../sonar/process/AesCipherTest/aes_secret_key.txt | 1 + .../sonar/process/AesCipherTest/bad_secret_key.txt | 1 + .../AesCipherTest/non_trimmed_secret_key.txt | 3 + .../process/AesCipherTest/other_secret_key.txt | 1 + .../sonar/process/LoggingTest/logback-access.xml | 1 + .../org/sonar/process/ProcessTest/sonar.properties | 212 ++++++++++ .../org/sonar/process/PropsTest/sonar.properties | 3 + .../src/test/resources/sonar-dummy-app.jar | Bin 0 -> 854048 bytes server/sonar-process/pom.xml | 108 +++++ .../src/main/java/org/sonar/process/AesCipher.java | 133 +++++++ .../main/java/org/sonar/process/Base64Cipher.java | 35 ++ .../src/main/java/org/sonar/process/Cipher.java | 27 ++ .../java/org/sonar/process/ConfigurationUtils.java | 70 ++++ .../main/java/org/sonar/process/Encryption.java | 64 +++ .../src/main/java/org/sonar/process/JmxUtils.java | 81 ++++ .../src/main/java/org/sonar/process/Lifecycle.java | 56 +++ .../java/org/sonar/process/LoopbackAddress.java | 71 ++++ .../java/org/sonar/process/MessageException.java | 36 ++ .../org/sonar/process/MinimumViableSystem.java | 86 ++++ .../java/org/sonar/process/MonitoredProcess.java | 31 ++ .../main/java/org/sonar/process/NetworkUtils.java | 40 ++ .../java/org/sonar/process/ProcessEntryPoint.java | 148 +++++++ .../java/org/sonar/process/ProcessLogging.java | 53 +++ .../main/java/org/sonar/process/ProcessMXBean.java | 28 ++ .../main/java/org/sonar/process/ProcessUtils.java | 77 ++++ .../src/main/java/org/sonar/process/Props.java | 120 ++++++ .../src/main/java/org/sonar/process/State.java | 26 ++ .../main/java/org/sonar/process/StopperThread.java | 57 +++ .../main/java/org/sonar/process/SystemExit.java | 52 +++ .../main/java/org/sonar/process/Terminable.java | 28 ++ .../main/java/org/sonar/process/package-info.java | 23 ++ .../test/java/org/sonar/process/AesCipherTest.java | 185 +++++++++ .../java/org/sonar/process/BaseProcessTest.java | 59 +++ .../org/sonar/process/ConfigurationUtilsTest.java | 95 +++++ .../java/org/sonar/process/EncryptionTest.java | 59 +++ .../test/java/org/sonar/process/JmxUtilsTest.java | 120 ++++++ .../test/java/org/sonar/process/LifecycleTest.java | 42 ++ .../org/sonar/process/LoopbackAddressTest.java | 51 +++ .../org/sonar/process/MinimumViableSystemTest.java | 102 +++++ .../java/org/sonar/process/NetworkUtilsTest.java | 61 +++ .../org/sonar/process/ProcessEntryPointTest.java | 224 +++++++++++ .../java/org/sonar/process/ProcessUtilsTest.java | 28 ++ .../src/test/java/org/sonar/process/PropsTest.java | 135 +++++++ .../java/org/sonar/process/SystemExitTest.java | 56 +++ .../java/org/sonar/process/test/HttpProcess.java | 116 ++++++ .../org/sonar/process/test/StandardProcess.java | 81 ++++ .../sonar/process/AesCipherTest/aes_secret_key.txt | 1 + .../sonar/process/AesCipherTest/bad_secret_key.txt | 1 + .../AesCipherTest/non_trimmed_secret_key.txt | 3 + .../process/AesCipherTest/other_secret_key.txt | 1 + .../sonar/process/LoggingTest/logback-access.xml | 1 + .../org/sonar/process/ProcessTest/sonar.properties | 212 ++++++++++ .../org/sonar/process/PropsTest/sonar.properties | 3 + .../src/test/resources/sonar-dummy-app.jar | Bin 0 -> 854048 bytes .../sonar-process/test-jar-with-dependencies.xml | 19 + .../main/java/org/sonar/search/SearchServer.java | 89 ++--- .../java/org/sonar/search/SearchServerTest.java | 6 +- .../java/org/sonar/server/app/EmbeddedTomcat.java | 79 ++-- .../main/java/org/sonar/server/app/WebServer.java | 29 +- .../src/main/java/org/sonar/server/app/Webapp.java | 1 - .../platform/PlatformServletContextListener.java | 6 +- .../server/plugins/ServerPluginJarsInstaller.java | 8 +- .../plugins/ServerPluginJarsInstallerTest.java | 38 +- .../org/sonar/server/search/BaseIndexTest.java | 3 +- .../java/org/sonar/server/tester/ServerTester.java | 10 +- sonar-application/pom.xml | 5 + .../src/main/java/org/sonar/application/App.java | 166 +++----- 93 files changed, 5650 insertions(+), 281 deletions(-) create mode 100644 server/process/sonar-process/src/test/java/org/sonar/process2/MonitorTest.java create mode 100644 server/sonar-process-monitor/pom.xml create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java create mode 100644 server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java create mode 100644 server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java create mode 100644 server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java create mode 100644 server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/InfiniteTerminationRmiConnector.java create mode 100644 server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java create mode 100644 server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java create mode 100644 server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java create mode 100644 server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java create mode 100644 server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java create mode 100644 server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java create mode 100644 server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt create mode 100644 server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt create mode 100644 server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt create mode 100644 server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt create mode 100644 server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml create mode 100644 server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties create mode 100644 server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties create mode 100644 server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar create mode 100644 server/sonar-process/pom.xml create mode 100644 server/sonar-process/src/main/java/org/sonar/process/AesCipher.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/Cipher.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/Encryption.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/MessageException.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/Props.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/State.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/StopperThread.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/SystemExit.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/Terminable.java create mode 100644 server/sonar-process/src/main/java/org/sonar/process/package-info.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/PropsTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java create mode 100644 server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java create mode 100644 server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt create mode 100644 server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt create mode 100644 server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt create mode 100644 server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt create mode 100644 server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml create mode 100644 server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties create mode 100644 server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties create mode 100644 server/sonar-process/src/test/resources/sonar-dummy-app.jar create mode 100644 server/sonar-process/test-jar-with-dependencies.xml (limited to 'sonar-application/pom.xml') diff --git a/server/pom.xml b/server/pom.xml index c0ec6370c42..55ee0ba06c5 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -11,7 +11,8 @@ SonarQube :: Server :: Parent - process + sonar-process + sonar-process-monitor sonar-search sonar-server sonar-web diff --git a/server/process/sonar-process/pom.xml b/server/process/sonar-process/pom.xml index 4753b693998..8e8236de387 100644 --- a/server/process/sonar-process/pom.xml +++ b/server/process/sonar-process/pom.xml @@ -69,36 +69,19 @@ test - diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java b/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java index f399318aa1e..e47c24682d4 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java +++ b/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java @@ -35,27 +35,37 @@ public class ProcessUtils { // only static stuff } + /** + * Do not abuse to this method. It uses exceptions to get status. + * @return false if process is null or terminated, else true. + */ public static boolean isAlive(@Nullable Process process) { - if (process == null) { - return false; - } - try { - process.exitValue(); - return false; - } catch (IllegalThreadStateException e) { - LOGGER.trace("Process has no exit value yet", e); - return true; + boolean alive = false; + if (process != null) { + try { + process.exitValue(); + } catch (IllegalThreadStateException ignored) { + alive = true; + } } + return alive; } - public static void destroyQuietly(@Nullable Process process) { - if (process != null && isAlive(process)) { + /** + * Destroys process (equivalent to kill -9) if alive + * @return true if the process was destroyed, false if process is null or already destroyed. + */ + public static boolean destroyQuietly(@Nullable Process process) { + boolean destroyed = false; + if (isAlive(process)) { try { process.destroy(); - } catch (Exception ignored) { - LOGGER.warn("Exception while destroying the process", ignored); + destroyed = true; + } catch (Exception e) { + LoggerFactory.getLogger(ProcessUtils.class).error("Fail to destroy " + process); } } + return destroyed; } public static void addSelfShutdownHook(final Terminable terminable) { diff --git a/server/process/sonar-process/src/test/java/org/sonar/process2/MonitorTest.java b/server/process/sonar-process/src/test/java/org/sonar/process2/MonitorTest.java new file mode 100644 index 00000000000..baaed0fe374 --- /dev/null +++ b/server/process/sonar-process/src/test/java/org/sonar/process2/MonitorTest.java @@ -0,0 +1,23 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process2; + +public class MonitorTest { +} diff --git a/server/sonar-process-monitor/pom.xml b/server/sonar-process-monitor/pom.xml new file mode 100644 index 00000000000..fa0869a52b8 --- /dev/null +++ b/server/sonar-process-monitor/pom.xml @@ -0,0 +1,87 @@ + + + + org.codehaus.sonar + server + 4.5-SNAPSHOT + ../ + + 4.0.0 + + sonar-process-monitor + SonarQube :: Process Monitor + + + + org.codehaus.sonar + sonar-process + ${project.version} + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + commons-codec + commons-codec + + + commons-io + commons-io + + + commons-lang + commons-lang + + + com.google.code.findbugs + jsr305 + provided + + + + junit + junit + test + + + org.easytesting + fest-assert + test + + + org.mockito + mockito-core + test + + + org.hamcrest + hamcrest-all + test + + + com.google.guava + guava + test + + + org.codehaus.sonar + sonar-process + test-jar + ${project.version} + + + com.github.kevinsawicki + http-request + test + + + + diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java new file mode 100644 index 00000000000..dd564d77247 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java @@ -0,0 +1,175 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import javax.annotation.Nullable; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +public class JavaCommand { + + // unique key among the group of commands to launch + private final String key; + + private File workDir; + + // any available port by default + private int jmxPort = -1; + + // for example -Xmx1G + private final List javaOptions = new ArrayList(); + + // entry point + private String className; + + // relative path to JAR files + private final List classpath = new ArrayList(); + + // program arguments (parameters of main(String[]) + private final Map arguments = new LinkedHashMap(); + + private final Map envVariables = new HashMap(System.getenv()); + + public JavaCommand(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public File getWorkDir() { + return workDir; + } + + public JavaCommand setWorkDir(File workDir) { + this.workDir = workDir; + return this; + } + + public JavaCommand setTempDir(File tempDir) { + this.javaOptions.add("-Djava.io.tmpdir=" + tempDir.getAbsolutePath()); + return this; + } + + public int getJmxPort() { + return jmxPort; + } + + public JavaCommand setJmxPort(int jmxPort) { + this.jmxPort = jmxPort; + return this; + } + + public List getJavaOptions() { + return javaOptions; + } + + public JavaCommand addJavaOption(String s) { + javaOptions.add(s); + return this; + } + + public JavaCommand addJavaOptions(String s) { + Collections.addAll(javaOptions, s.split(" ")); + return this; + } + + public String getClassName() { + return className; + } + + public JavaCommand setClassName(String className) { + this.className = className; + return this; + } + + public List getClasspath() { + return classpath; + } + + public JavaCommand addClasspath(String s) { + classpath.add(s); + return this; + } + + public Map getArguments() { + return arguments; + } + + public JavaCommand setArgument(String key, @Nullable String value) { + if (value == null) { + arguments.remove(key); + } else { + arguments.put(key, value); + } + return this; + } + + public JavaCommand setArguments(Properties args) { + for (Map.Entry entry : args.entrySet()) { + setArgument(entry.getKey().toString(), entry.getValue() != null ? entry.getValue().toString() : null); + } + return this; + } + + public Map getEnvVariables() { + return envVariables; + } + + public JavaCommand setEnvVariable(String key, @Nullable String value) { + if (value == null) { + envVariables.remove(key); + } else { + envVariables.put(key, value); + } + return this; + } + + public boolean isDebugMode() { + for (String javaOption : javaOptions) { + if (javaOption.contains("-agentlib:jdwp")) { + return true; + } + } + return false; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("JavaCommand{"); + sb.append("workDir=").append(workDir); + sb.append(", jmxPort=").append(jmxPort); + sb.append(", javaOptions=").append(javaOptions); + sb.append(", className='").append(className).append('\''); + sb.append(", classpath=").append(classpath); + sb.append(", arguments=").append(arguments); + sb.append(", envVariables=").append(envVariables); + sb.append('}'); + return sb.toString(); + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java new file mode 100644 index 00000000000..ff8ef88e8f7 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java @@ -0,0 +1,124 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.LoggerFactory; +import org.sonar.process.LoopbackAddress; +import org.sonar.process.ProcessEntryPoint; +import org.sonar.process.ProcessUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +public class JavaProcessLauncher { + + private final Timeouts timeouts; + + public JavaProcessLauncher(Timeouts timeouts) { + this.timeouts = timeouts; + } + + ProcessRef launch(JavaCommand command) { + Process process = null; + try { + ProcessBuilder processBuilder = create(command); + LoggerFactory.getLogger(getClass()).info("Launch {}: {}", + command.getKey(), StringUtils.join(processBuilder.command(), " ")); + process = processBuilder.start(); + StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), command.getKey()); + StreamGobbler inputGobbler = new StreamGobbler(process.getInputStream(), command.getKey()); + inputGobbler.start(); + errorGobbler.start(); + + return new ProcessRef(command.getKey(), process, errorGobbler, inputGobbler); + + } catch (Exception e) { + // just in case + ProcessUtils.destroyQuietly(process); + throw new IllegalStateException("Fail to launch " + command.getKey(), e); + } + } + + private ProcessBuilder create(JavaCommand javaCommand) { + List commands = new ArrayList(); + commands.add(buildJavaPath()); + commands.addAll(javaCommand.getJavaOptions()); + commands.addAll(buildJmxOptions(javaCommand)); + commands.addAll(buildClasspath(javaCommand)); + commands.add(javaCommand.getClassName()); + + // TODO warning - does it work if temp dir contains a whitespace ? + commands.add(buildPropertiesFile(javaCommand).getAbsolutePath()); + + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.command(commands); + processBuilder.directory(javaCommand.getWorkDir()); + processBuilder.environment().putAll(javaCommand.getEnvVariables()); + return processBuilder; + } + + private String buildJavaPath() { + String separator = System.getProperty("file.separator"); + return new File(new File(System.getProperty("java.home")), + "bin" + separator + "java").getAbsolutePath(); + } + + private List buildJmxOptions(JavaCommand javaCommand) { + if (javaCommand.getJmxPort() < 1) { + throw new IllegalStateException("JMX port is not set"); + } + return Arrays.asList( + "-Dcom.sun.management.jmxremote", + "-Dcom.sun.management.jmxremote.port=" + javaCommand.getJmxPort(), + "-Dcom.sun.management.jmxremote.authenticate=false", + "-Dcom.sun.management.jmxremote.ssl=false", + "-Djava.rmi.server.hostname=" + LoopbackAddress.get().getHostAddress()); + } + + private List buildClasspath(JavaCommand javaCommand) { + return Arrays.asList("-cp", StringUtils.join(javaCommand.getClasspath(), System.getProperty("path.separator"))); + } + + private File buildPropertiesFile(JavaCommand javaCommand) { + File propertiesFile = null; + try { + propertiesFile = File.createTempFile("sq-conf", "properties"); + Properties props = new Properties(); + props.putAll(javaCommand.getArguments()); + props.setProperty(ProcessEntryPoint.PROPERTY_PROCESS_KEY, javaCommand.getKey()); + props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, String.valueOf(javaCommand.isDebugMode())); + props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_TIMEOUT, String.valueOf(timeouts.getAutokillPingTimeout())); + props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_INTERVAL, String.valueOf(timeouts.getAutokillPingInterval())); + props.setProperty(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, String.valueOf(timeouts.getTerminationTimeout())); + OutputStream out = new FileOutputStream(propertiesFile); + props.store(out, String.format("Temporary properties file for command [%s]", javaCommand.getKey())); + out.close(); + return propertiesFile; + } catch (Exception e) { + throw new IllegalStateException("Cannot write temporary settings to " + propertiesFile, e); + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java new file mode 100644 index 00000000000..b06ea684d41 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java @@ -0,0 +1,35 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +/** + * Interactions with monitored process + */ +public interface JmxConnector { + + void connect(JavaCommand command, ProcessRef processRef); + + void ping(ProcessRef process); + + boolean isReady(ProcessRef process); + + void terminate(ProcessRef process); + +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java new file mode 100644 index 00000000000..ccf2af343dc --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java @@ -0,0 +1,208 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.slf4j.LoggerFactory; +import org.sonar.process.Lifecycle; +import org.sonar.process.MessageException; +import org.sonar.process.State; +import org.sonar.process.SystemExit; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class Monitor { + + private final List processes = new CopyOnWriteArrayList(); + private final TerminatorThread terminator; + private final JavaProcessLauncher launcher; + private final JmxConnector jmxConnector; + private final Lifecycle lifecycle = new Lifecycle(); + private final Timeouts timeouts; + + private final SystemExit systemExit; + private Thread shutdownHook = new Thread(new MonitorShutdownHook(), "Monitor Shutdown Hook"); + + // used by awaitTermination() to block until all processes are shutdown + private final List watcherThreads = new CopyOnWriteArrayList(); + + Monitor(JavaProcessLauncher launcher, JmxConnector jmxConnector, Timeouts timeouts, SystemExit exit) { + this.launcher = launcher; + this.jmxConnector = jmxConnector; + this.timeouts = timeouts; + this.terminator = new TerminatorThread(processes, jmxConnector, timeouts); + this.systemExit = exit; + } + + public static Monitor create() { + Timeouts timeouts = new Timeouts(); + return new Monitor(new JavaProcessLauncher(timeouts), new RmiJmxConnector(timeouts), + timeouts, new SystemExit()); + } + + /** + * Starts commands and blocks current thread until all processes are in state {@link State#STARTED}. + * @throws java.lang.IllegalArgumentException if commands list is empty + * @throws java.lang.IllegalStateException if already started or if at least one process failed to start. In this case + * all processes are terminated. No need to execute {@link #stop()} + */ + public void start(List commands) { + if (commands.isEmpty()) { + throw new IllegalArgumentException("At least one command is required"); + } + + if (!lifecycle.tryToMoveTo(State.STARTING)) { + throw new IllegalStateException("Can not start multiple times"); + } + + // intercepts CTRL-C + Runtime.getRuntime().addShutdownHook(shutdownHook); + + for (JavaCommand command : commands) { + try { + ProcessRef processRef = launcher.launch(command); + monitor(command, processRef); + } catch (RuntimeException e) { + // fail to start or to monitor + stop(); + throw e; + } + } + + if (!lifecycle.tryToMoveTo(State.STARTED)) { + // stopping or stopped during startup, for instance : + // 1. A is started + // 2. B starts + // 3. A crashes while B is starting + // 4. if B was not monitored during Terminator execution, then it's an alive orphan + stop(); + throw new IllegalStateException("Stopped during startup"); + } + } + + private void monitor(JavaCommand command, ProcessRef processRef) { + // physically watch if process is alive + WatcherThread watcherThread = new WatcherThread(processRef, this); + watcherThread.start(); + watcherThreads.add(watcherThread); + + // add to list of monitored processes only when successfully connected to it + jmxConnector.connect(command, processRef); + processes.add(processRef); + + // ping process on a regular basis + processRef.setPingEnabled(!command.isDebugMode()); + if (processRef.isPingEnabled()) { + PingerThread.startPinging(processRef, jmxConnector, timeouts); + } + + // wait for process to be ready (accept requests or so on) + waitForReady(processRef); + + LoggerFactory.getLogger(getClass()).info(String.format("%s is up", processRef)); + } + + private void waitForReady(ProcessRef processRef) { + boolean ready = false; + while (!ready) { + if (processRef.isTerminated()) { + throw new MessageException(String.format("%s failed to start", processRef)); + } + try { + ready = jmxConnector.isReady(processRef); + } catch (Exception ignored) { + // pb with the JMX connection, can occur if RMI not initialized yet + } + try { + Thread.sleep(300L); + } catch (InterruptedException e) { + throw new IllegalStateException("Interrupted while waiting for " + processRef + " to be ready", e); + } + } + } + + /** + * Blocks until all processes are terminated + */ + public void awaitTermination() { + for (WatcherThread watcherThread : watcherThreads) { + while (watcherThread.isAlive()) { + try { + watcherThread.join(); + } catch (InterruptedException ignored) { + // ignore, stop blocking + } + } + } + } + + /** + * Blocks until all processes are terminated. + */ + public void stop() { + terminateAsync(); + try { + terminator.join(); + } catch (InterruptedException ignored) { + // ignore, stop blocking + } + // safeguard if TerminatorThread is buggy + hardKillAll(); + lifecycle.tryToMoveTo(State.STOPPED); + systemExit.exit(0); + } + + /** + * Asks for processes termination and returns without blocking until termination. + * @return true if termination was requested, false if it was already being terminated + */ + boolean terminateAsync() { + boolean requested = false; + if (lifecycle.tryToMoveTo(State.STOPPING)) { + requested = true; + terminator.start(); + } + return requested; + } + + private void hardKillAll() { + // no specific order, kill'em all!!! + for (ProcessRef process : processes) { + process.hardKill(); + } + } + + public State getState() { + return lifecycle.getState(); + } + + Thread getShutdownHook() { + return shutdownHook; + } + + private class MonitorShutdownHook implements Runnable { + @Override + public void run() { + systemExit.setInShutdownHook(); + // blocks until everything is corrected terminated + stop(); + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java new file mode 100644 index 00000000000..65df8546e33 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java @@ -0,0 +1,60 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * This thread pings a process - through RMI - at fixed delay + */ +class PingerThread extends Thread { + + private final ProcessRef processRef; + private final JmxConnector jmxConnector; + + private PingerThread(ProcessRef process, JmxConnector jmxConnector) { + // it's important to give a name for traceability in profiling tools like visualVM + super(String.format("Ping[%s]", process.getKey())); + setDaemon(true); + this.processRef = process; + this.jmxConnector = jmxConnector; + } + + @Override + public void run() { + if (!processRef.isTerminated() && processRef.isPingEnabled()) { + try { + jmxConnector.ping(processRef); + } catch (Exception ignored) { + // failed to ping + } + } else { + interrupt(); + } + } + + static void startPinging(ProcessRef processRef, JmxConnector jmxConnector, Timeouts timeouts) { + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + PingerThread pinger = new PingerThread(processRef, jmxConnector); + scheduler.scheduleAtFixedRate(pinger, 0L, timeouts.getMonitorPingInterval(), TimeUnit.MILLISECONDS); + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java new file mode 100644 index 00000000000..8d926c39d72 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java @@ -0,0 +1,98 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.sonar.process.ProcessUtils; + +class ProcessRef { + + private final String key; + private final Process process; + private final StreamGobbler[] gobblers; + private volatile boolean terminated = false; + private volatile boolean pingEnabled = true; + + ProcessRef(String key, Process process, StreamGobbler... gobblers) { + this.key = key; + this.process = process; + this.terminated = !ProcessUtils.isAlive(process); + this.gobblers = gobblers; + } + + /** + * Unique logical key (not the pid), for instance "ES" + */ + String getKey() { + return key; + } + + /** + * The {@link java.lang.Process} + */ + Process getProcess() { + return process; + } + + /** + * Almost real-time status + */ + boolean isTerminated() { + return terminated; + } + + /** + * Sending pings can be disabled when requesting for termination or when process is on debug mode (JDWP) + */ + void setPingEnabled(boolean b) { + this.pingEnabled = b; + } + + boolean isPingEnabled() { + return pingEnabled; + } + + /** + * Destroy the process without gracefully asking it to terminate (kill -9). + * @return true if the process was killed, false if process is already terminated + */ + boolean hardKill() { + boolean killed = false; + terminated = true; + pingEnabled = false; + if (ProcessUtils.isAlive(process)) { + ProcessUtils.destroyQuietly(process); + killed = true; + } + for (StreamGobbler gobbler : gobblers) { + StreamGobbler.waitUntilFinish(gobbler); + } + ProcessUtils.closeStreams(process); + return killed; + } + + void setTerminated(boolean b) { + this.terminated = b; + } + + @Override + public String toString() { + return String.format("Process[%s]", key); + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java new file mode 100644 index 00000000000..d8889d9c45e --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java @@ -0,0 +1,133 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.sonar.process.JmxUtils; +import org.sonar.process.LoopbackAddress; +import org.sonar.process.ProcessMXBean; +import org.sonar.process.ProcessUtils; + +import javax.annotation.CheckForNull; +import javax.management.JMX; +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +class RmiJmxConnector implements JmxConnector { + + static { + /* + Prevents such warnings : + + WARNING: Failed to restart: java.io.IOException: Failed to get a RMI stub: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is: + java.net.ConnectException: Connection refused] + Sep 11, 2014 7:32:32 PM RMIConnector RMIClientCommunicatorAdmin-doStop + WARNING: Failed to call the method close():java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is: + java.net.ConnectException: Connection refused + Sep 11, 2014 7:32:32 PM ClientCommunicatorAdmin Checker-run + WARNING: Failed to check connection: java.net.ConnectException: Connection refused + Sep 11, 2014 7:32:32 PM ClientCommunicatorAdmin Checker-run + WARNING: stopping + */ + System.setProperty("sun.rmi.transport.tcp.logLevel", "SEVERE"); + } + + private final Map mbeans = new IdentityHashMap(); + private final Timeouts timeouts; + + RmiJmxConnector(Timeouts timeouts) { + this.timeouts = timeouts; + } + + @Override + public synchronized void connect(final JavaCommand command, ProcessRef processRef) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + ConnectorCallable callable = new ConnectorCallable(command, processRef.getProcess()); + try { + Future future = executor.submit(callable); + ProcessMXBean mxBean = future.get(timeouts.getJmxConnectionTimeout(), TimeUnit.MILLISECONDS); + if (mxBean != null) { + mbeans.put(processRef, mxBean); + } + } catch (Exception e) { + if (callable.latestException != null) { + throw callable.latestException; + } + throw new IllegalStateException("Fail to connect to JMX", e); + } finally { + executor.shutdownNow(); + } + } + + @Override + public void ping(ProcessRef processRef) { + mbeans.get(processRef).ping(); + } + + @Override + public boolean isReady(ProcessRef processRef) { + return mbeans.get(processRef).isReady(); + } + + @Override + public void terminate(ProcessRef processRef) { + mbeans.get(processRef).terminate(); + } + + private static class ConnectorCallable implements Callable { + private final JavaCommand command; + private final Process process; + private RuntimeException latestException; + + private ConnectorCallable(JavaCommand command, Process process) { + this.command = command; + this.process = process; + } + + @Override + @CheckForNull + public ProcessMXBean call() throws Exception { + JMXServiceURL jmxUrl = JmxUtils.serviceUrl(LoopbackAddress.get(), command.getJmxPort()); + while (ProcessUtils.isAlive(process)) { + try { + JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, null); + MBeanServerConnection mBeanServer = jmxConnector.getMBeanServerConnection(); + return JMX.newMBeanProxy(mBeanServer, JmxUtils.objectName(command.getKey()), ProcessMXBean.class); + } catch (Exception e) { + latestException = new IllegalStateException(String.format( + "Fail to connect to JMX bean of %s [%s] ", command.getKey(), jmxUrl), e); + } + Thread.sleep(300L); + } + + // process went down, no need to connect + return null; + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java new file mode 100644 index 00000000000..55d95c8a467 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java @@ -0,0 +1,69 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * Reads process output and writes to logs + */ +class StreamGobbler extends Thread { + + private final InputStream is; + private final Logger logger; + + StreamGobbler(InputStream is, String processKey) { + super(String.format("Gobbler[%s]", processKey)); + this.is = is; + this.logger = LoggerFactory.getLogger(processKey); + } + + @Override + public void run() { + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + try { + String line; + while ((line = br.readLine()) != null) { + logger.info(line); + } + } catch (Exception ignored) { + + } finally { + IOUtils.closeQuietly(br); + } + } + + static void waitUntilFinish(@Nullable StreamGobbler gobbler) { + if (gobbler != null) { + try { + gobbler.join(); + } catch (InterruptedException ignored) { + } + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java new file mode 100644 index 00000000000..775a036bb2f --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java @@ -0,0 +1,78 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * Terminates all monitored processes. Tries to gracefully terminate each process, + * then kill if timeout expires. Ping monitoring is disabled so process auto kills (self graceful termination, else self kill) + * if it does not receive the termination request. + */ +class TerminatorThread extends Thread { + + private final List processes; + private final JmxConnector jmxConnector; + private final Timeouts timeouts; + + TerminatorThread(List processes, JmxConnector jmxConnector, Timeouts timeouts) { + super("Terminator"); + this.processes = processes; + this.jmxConnector = jmxConnector; + this.timeouts = timeouts; + } + + @Override + public void run() { + // terminate in reverse order of startup (dependency order) + for (int index = processes.size() - 1; index >= 0; index--) { + final ProcessRef processRef = processes.get(index); + if (!processRef.isTerminated()) { + processRef.setPingEnabled(false); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(new Runnable() { + @Override + public void run() { + // ask for graceful termination + LoggerFactory.getLogger(getClass()).info("Request termination of " + processRef); + jmxConnector.terminate(processRef); + } + }); + try { + future.get(timeouts.getTerminationTimeout(), TimeUnit.MILLISECONDS); + } catch (Exception ignored) { + // failed to gracefully stop in a timely fashion + LoggerFactory.getLogger(getClass()).info(String.format("Kill %s", processRef)); + } finally { + executor.shutdownNow(); + // kill even if graceful termination was done, just to be sure that physical process is really down + processRef.hardKill(); + } + } + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java new file mode 100644 index 00000000000..30ab3f5ce0c --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java @@ -0,0 +1,103 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +/** + * Most of the timeouts involved in process monitoring, in milliseconds + */ +class Timeouts { + + private long terminationTimeout = 120000L; + private long jmxConnectionTimeout = 30000L; + private long monitorPingInterval = 3000L; + private long autokillPingTimeout = 60000L; + private long autokillPingInterval = 3000L; + + /** + * [monitor] Timeout to get connected to RMI MXBean while process is alive + */ + long getJmxConnectionTimeout() { + return jmxConnectionTimeout; + } + + /** + * @see #getJmxConnectionTimeout() + */ + void setJmxConnectionTimeout(long l) { + this.jmxConnectionTimeout = l; + } + + /** + * [monitor] Delay between each ping request + */ + long getMonitorPingInterval() { + return monitorPingInterval; + } + + /** + * @see #getMonitorPingInterval() + */ + void setMonitorPingInterval(long l) { + this.monitorPingInterval = l; + } + + /** + * [monitored process] maximum age of last received ping before process autokills + */ + long getAutokillPingTimeout() { + return autokillPingTimeout; + } + + /** + * @see #getAutokillPingTimeout() + */ + void setAutokillPingTimeout(long l) { + this.autokillPingTimeout = l; + } + + /** + * [monitored process] delay between checks of freshness of received pings + */ + long getAutokillPingInterval() { + return autokillPingInterval; + } + + /** + * @see #getAutokillPingInterval() + */ + void setAutokillPingInterval(long l) { + this.autokillPingInterval = l; + } + + /** + * [both monitor and monitored process] timeout of graceful termination before hard killing + */ + long getTerminationTimeout() { + return terminationTimeout; + } + + /** + * @see #getTerminationTimeout() + */ + void setTerminationTimeout(long l) { + this.terminationTimeout = l; + } + +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java new file mode 100644 index 00000000000..a0b92f5ddc8 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java @@ -0,0 +1,70 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.slf4j.LoggerFactory; +import org.sonar.process.ProcessUtils; + +/** + * This thread blocks as long as the monitored process is physically alive. + * It avoids from executing {@link Process#exitValue()} at a fixed rate : + *
    + *
  • no usage of exception for flow control. Indeed {@link Process#exitValue()} throws an exception + * if process is alive. There's no method Process#isAlive()
  • + *
  • no delay, instantaneous notification that process is down
  • + *
+ */ +class WatcherThread extends Thread { + + private final ProcessRef process; + private final Monitor monitor; + + WatcherThread(ProcessRef processRef, Monitor monitor) { + // this name is different than Thread#toString(), which includes name, priority + // and thread group + // -> do not override toString() + super(String.format("Watch[%s]", processRef.getKey())); + this.process = processRef; + this.monitor = monitor; + } + + @Override + public void run() { + boolean alive = true; + while (alive) { + try { + process.getProcess().waitFor(); + process.setTerminated(true); + LoggerFactory.getLogger(getClass()).info(process + " is down"); + // terminate all other processes, but in another thread + monitor.stop(); + alive = false; + } catch (InterruptedException ignored) { + if (ProcessUtils.isAlive(process.getProcess())) { + LoggerFactory.getLogger(getClass()).error(String.format( + "Watcher of [%s] was interrupted but process is still alive. Killing it.", process.getKey())); + } + alive = false; + } finally { + process.hardKill(); + } + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java new file mode 100644 index 00000000000..93fcb831c83 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.process.monitor; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java new file mode 100644 index 00000000000..3c0c7be0208 --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java @@ -0,0 +1,38 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +/** + * Used to verify that pings were sent or not. + */ +public class CallVerifierJmxConnector extends RmiJmxConnector { + + boolean askedPing = false; + + CallVerifierJmxConnector(Timeouts timeouts) { + super(timeouts); + } + + @Override + public void ping(ProcessRef process) { + askedPing = true; + super.ping(process); + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java new file mode 100644 index 00000000000..0df9ee9f023 --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java @@ -0,0 +1,42 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +public class ImpossibleToConnectJmxConnector implements JmxConnector { + @Override + public void connect(JavaCommand command, ProcessRef processRef) { + throw new IllegalStateException("Test - Impossible to connect to JMX"); + } + + @Override + public void ping(ProcessRef process) { + + } + + @Override + public boolean isReady(ProcessRef process) { + return false; + } + + @Override + public void terminate(ProcessRef process) { + + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/InfiniteTerminationRmiConnector.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/InfiniteTerminationRmiConnector.java new file mode 100644 index 00000000000..b252379ae8d --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/InfiniteTerminationRmiConnector.java @@ -0,0 +1,38 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +public class InfiniteTerminationRmiConnector extends RmiJmxConnector { + + InfiniteTerminationRmiConnector(Timeouts timeouts) { + super(timeouts); + } + + @Override + public void terminate(ProcessRef processRef) { + try { + while (true) { + Thread.sleep(50L); + } + } catch (Exception e) { + + } + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java new file mode 100644 index 00000000000..66654b0f1fc --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java @@ -0,0 +1,79 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.util.Properties; + +import static org.fest.assertions.Assertions.assertThat; + +public class JavaCommandTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void test_parameters() throws Exception { + JavaCommand command = new JavaCommand("es"); + + command.setArgument("first_arg", "val1"); + Properties args = new Properties(); + args.setProperty("second_arg", "val2"); + command.setArguments(args); + + command.setJmxPort(1234); + command.setClassName("org.sonar.ElasticSearch"); + command.setEnvVariable("BUILD_ID", "1000"); + File tempDir = temp.newFolder(); + command.setTempDir(tempDir); + File workDir = temp.newFolder(); + command.setWorkDir(workDir); + command.addClasspath("lib/*.jar"); + command.addClasspath("conf/*.xml"); + command.addJavaOption("-Xmx128m"); + + assertThat(command.toString()).isNotNull(); + assertThat(command.getClasspath()).containsOnly("lib/*.jar", "conf/*.xml"); + assertThat(command.getJavaOptions()).containsOnly("-Xmx128m", "-Djava.io.tmpdir=" + tempDir.getAbsolutePath()); + assertThat(command.getWorkDir()).isSameAs(workDir); + assertThat(command.getJmxPort()).isEqualTo(1234); + assertThat(command.getClassName()).isEqualTo("org.sonar.ElasticSearch"); + assertThat(command.getEnvVariables().get("BUILD_ID")).isEqualTo("1000"); + + // copy current env variables + assertThat(command.getEnvVariables().size()).isGreaterThan(1); + } + + @Test + public void test_debug_mode() throws Exception { + JavaCommand command = new JavaCommand("es"); + assertThat(command.isDebugMode()).isFalse(); + + command.addJavaOption("-Xmx512m"); + assertThat(command.isDebugMode()).isFalse(); + + command.addJavaOption("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"); + assertThat(command.isDebugMode()).isTrue(); + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java new file mode 100644 index 00000000000..80a02cd39c6 --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java @@ -0,0 +1,43 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.junit.Test; +import org.sonar.process.NetworkUtils; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + +public class JavaProcessLauncherTest { + + @Test + public void fail_to_launch() throws Exception { + JavaCommand command = new JavaCommand("test").setJmxPort(NetworkUtils.freePort()); + JavaProcessLauncher launcher = new JavaProcessLauncher(new Timeouts()); + try { + // command is not correct (missing options), java.lang.ProcessBuilder#start() + // throws an exception + launcher.launch(command); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Fail to launch test"); + } + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java new file mode 100644 index 00000000000..291b91fbad4 --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java @@ -0,0 +1,442 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import com.github.kevinsawicki.http.HttpRequest; +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.Timeout; +import org.sonar.process.NetworkUtils; +import org.sonar.process.State; +import org.sonar.process.SystemExit; + +import java.io.File; +import java.io.IOException; +import java.net.ConnectException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Mockito.mock; + +public class MonitorTest { + + static File testJar; + Monitor monitor; + SystemExit exit = mock(SystemExit.class); + + /** + * Find the JAR file containing the test apps. Classes can't be moved in sonar-process-monitor because + * they require sonar-process dependencies when executed here (sonar-process, commons-*, ...). + */ + @BeforeClass + public static void initTestJar() { + File targetDir = new File("server/sonar-process/target"); + if (!targetDir.exists() || !targetDir.isDirectory()) { + targetDir = new File("../sonar-process/target"); + } + if (!targetDir.exists() || !targetDir.isDirectory()) { + throw new IllegalStateException("target dir of sonar-process module not found. Please build it."); + } + Collection jars = FileUtils.listFiles(targetDir, new String[] {"jar"}, false); + for (File jar : jars) { + if (jar.getName().startsWith("sonar-process-") && jar.getName().endsWith("-test-jar-with-dependencies.jar")) { + testJar = jar; + return; + } + } + throw new IllegalStateException("No sonar-process-*-test-jar-with-dependencies.jar in " + targetDir); + } + + /** + * Safeguard + */ + @Rule + public Timeout globalTimeout = new Timeout(10000); + + /** + * Temporary directory is used to interact with monitored processes, which write in it. + */ + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + /** + * Safeguard + */ + @After + public void tearDown() throws Exception { + try { + if (monitor != null) { + monitor.stop(); + } + } catch (Throwable ignored) { + } + } + + @Test + public void fail_to_start_if_no_commands() throws Exception { + monitor = newDefaultMonitor(); + try { + monitor.start(Collections.emptyList()); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("At least one command is required"); + } + } + + @Test + public void fail_to_start_multiple_times() throws Exception { + monitor = newDefaultMonitor(); + monitor.start(Arrays.asList(newStandardProcessCommand())); + boolean failed = false; + try { + monitor.start(Arrays.asList(newStandardProcessCommand())); + } catch (IllegalStateException e) { + failed = e.getMessage().equals("Can not start multiple times"); + } + monitor.stop(); + assertThat(failed); + } + + @Test + public void start_then_stop_gracefully() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient client = new HttpProcessClient("test"); + // blocks until started + monitor.start(Arrays.asList(client.newCommand())); + + assertThat(client.isReady()).isTrue(); + assertThat(client.wasReadyAt()).isLessThanOrEqualTo(System.currentTimeMillis()); + + // blocks until stopped + monitor.stop(); + assertThat(client.isReady()).isFalse(); + assertThat(client.wasGracefullyTerminated()).isTrue(); + assertThat(monitor.getState()).isEqualTo(State.STOPPED); + } + + @Test + public void start_then_stop_sequence_of_commands() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2"); + monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); + + // start p2 when p1 is fully started (ready) + assertThat(p1.isReady()).isTrue(); + assertThat(p2.isReady()).isTrue(); + assertThat(p2.wasStartingAt()).isGreaterThanOrEqualTo(p1.wasReadyAt()); + + monitor.stop(); + + // stop in inverse order + assertThat(p1.isReady()).isFalse(); + assertThat(p2.isReady()).isFalse(); + assertThat(p1.wasGracefullyTerminated()).isTrue(); + assertThat(p2.wasGracefullyTerminated()).isTrue(); + assertThat(p2.wasGracefullyTerminatedAt()).isLessThanOrEqualTo(p1.wasGracefullyTerminatedAt()); + } + + @Test + public void fail_to_connect_to_jmx() throws Exception { + Timeouts timeouts = new Timeouts(); + monitor = new Monitor(new JavaProcessLauncher(timeouts), + new ImpossibleToConnectJmxConnector(), timeouts, exit); + + HttpProcessClient p1 = new HttpProcessClient("p1"); + try { + monitor.start(Arrays.asList(p1.newCommand())); + fail(); + } catch (Exception e) { + // process was correctly launched, but there was a problem with RMI + assertThat(p1.isReady()).isFalse(); + assertThat(p1.wasGracefullyTerminated()).isFalse(); + } + } + + @Test + public void terminate_all_processes_if_monitor_shutdowns() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2"); + monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); + assertThat(p1.isReady()).isTrue(); + assertThat(p2.isReady()).isTrue(); + + // emulate CTRL-C + monitor.getShutdownHook().run(); + monitor.getShutdownHook().join(); + + assertThat(p1.wasGracefullyTerminated()).isTrue(); + assertThat(p2.wasGracefullyTerminated()).isTrue(); + } + + @Test + public void terminate_all_processes_if_one_monitored_process_shutdowns() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2"); + monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); + assertThat(p1.isReady()).isTrue(); + assertThat(p2.isReady()).isTrue(); + + // kill p1 -> waiting for detection by monitor than termination of p2 + p1.kill(); + monitor.awaitTermination(); + + assertThat(p1.isReady()).isFalse(); + assertThat(p2.isReady()).isFalse(); + assertThat(p1.wasGracefullyTerminated()).isFalse(); + assertThat(p2.wasGracefullyTerminated()).isTrue(); + } + + @Test + public void terminate_all_processes_if_one_fails_to_start() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2", -1, NetworkUtils.freePort()); + try { + monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); + fail(); + } catch (Exception expected) { + assertThat(p1.wasReady()).isTrue(); + assertThat(p2.wasReady()).isFalse(); + assertThat(p1.wasGracefullyTerminated()).isTrue(); + // self "gracefully terminated", even if startup went bad + assertThat(p2.wasGracefullyTerminated()).isTrue(); + } + } + + @Test + public void kill_process_if_too_long_to_request_gracefully_termination() throws Exception { + Timeouts timeouts = new Timeouts(); + timeouts.setTerminationTimeout(100L); + monitor = new Monitor(new JavaProcessLauncher(timeouts), + new InfiniteTerminationRmiConnector(timeouts), timeouts, exit); + + HttpProcessClient p1 = new HttpProcessClient("p1"); + monitor.start(Arrays.asList(p1.newCommand())); + assertThat(p1.isReady()).isTrue(); + + monitor.stop(); + assertThat(p1.isReady()).isFalse(); + } + + @Test + public void kill_process_if_fail_to_request_gracefully_termination() throws Exception { + Timeouts timeouts = new Timeouts(); + timeouts.setTerminationTimeout(100L); + monitor = new Monitor(new JavaProcessLauncher(timeouts), + new TerminationFailureRmiConnector(timeouts), timeouts, exit); + + HttpProcessClient p1 = new HttpProcessClient("p1"); + monitor.start(Arrays.asList(p1.newCommand())); + assertThat(p1.isReady()).isTrue(); + + monitor.stop(); + assertThat(p1.isReady()).isFalse(); + } + + @Test + public void fail_to_start_if_bad_class_name() throws Exception { + monitor = newDefaultMonitor(); + JavaCommand command = new JavaCommand("test") + .addClasspath(testJar.getAbsolutePath()) + .setClassName("org.sonar.process.test.Unknown") + .setJmxPort(NetworkUtils.freePort()) + .setTempDir(temp.newFolder()); + + try { + monitor.start(Arrays.asList(command)); + fail(); + } catch (Exception e) { + // expected + // TODO improve, too many stacktraces logged + } + } + + @Test + public void terminate_all_if_one_monitored_process_shutdowns() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient client = new HttpProcessClient("test"); + // blocks until started + monitor.start(Arrays.asList(client.newCommand())); + assertThat(client.isReady()).isTrue(); + + client.kill(); + assertThat(client.isReady()).isFalse(); + + // does not wait, already terminated + monitor.awaitTermination(); + + // TODO check logs + } + + @Test + public void fail_if_jmx_port_is_not_available() throws Exception { + monitor = newDefaultMonitor(); + // c1 and c2 have same JMX port + int jmxPort = NetworkUtils.freePort(); + HttpProcessClient p1 = new HttpProcessClient("p1", NetworkUtils.freePort(), jmxPort); + HttpProcessClient p2 = new HttpProcessClient("p2", NetworkUtils.freePort(), jmxPort); + try { + monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); + fail(); + } catch (Exception expected) { + assertThat(p1.wasReady()).isTrue(); + assertThat(p2.wasReady()).isFalse(); + assertThat(p1.isReady()).isFalse(); + assertThat(p2.isReady()).isFalse(); + } + } + + @Test + public void disable_autokill_on_jvm_debug_mode() throws Exception { + Timeouts timeouts = new Timeouts(); + timeouts.setMonitorPingInterval(10L); + timeouts.setAutokillPingInterval(10L); + timeouts.setAutokillPingTimeout(10L); + CallVerifierJmxConnector jmxConnector = new CallVerifierJmxConnector(timeouts); + monitor = new Monitor(new JavaProcessLauncher(timeouts), jmxConnector, timeouts, exit); + + JavaCommand command = newStandardProcessCommand() + .addJavaOption("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=" + NetworkUtils.freePort()); + monitor.start(Arrays.asList(command)); + + Thread.sleep(20L); + assertThat(jmxConnector.askedPing).isFalse(); + + monitor.stop(); + } + + private Monitor newDefaultMonitor() { + Timeouts timeouts = new Timeouts(); + return new Monitor(new JavaProcessLauncher(timeouts), new RmiJmxConnector(timeouts), timeouts, exit); + } + + /** + * Interaction with {@link org.sonar.process.test.HttpProcess} + */ + private class HttpProcessClient { + private final int httpPort; + private final String commandKey; + private final File tempDir; + private int jmxPort; + + private HttpProcessClient(String commandKey) throws IOException { + this(commandKey, NetworkUtils.freePort(), NetworkUtils.freePort()); + } + + /** + * Use httpPort=-1 to make server fail to start + */ + private HttpProcessClient(String commandKey, int httpPort, int jmxPort) throws IOException { + this.commandKey = commandKey; + this.tempDir = temp.newFolder(commandKey); + this.httpPort = httpPort; + this.jmxPort = jmxPort; + } + + JavaCommand newCommand() throws IOException { + return new JavaCommand(commandKey) + .addClasspath(testJar.getAbsolutePath()) + .setClassName("org.sonar.process.test.HttpProcess") + .setJmxPort(jmxPort) + .setArgument("httpPort", String.valueOf(httpPort)) + .setTempDir(tempDir); + } + + /** + * @see org.sonar.process.test.HttpProcess + */ + boolean isReady() { + try { + HttpRequest httpRequest = HttpRequest.get("http://localhost:" + httpPort + "/ping") + .readTimeout(500).connectTimeout(500); + return httpRequest.ok() && httpRequest.body().equals("ping"); + } catch (HttpRequest.HttpRequestException e) { + if (e.getCause() instanceof ConnectException) { + return false; + } + throw new IllegalStateException("Fail to know the process status", e); + } + } + + /** + * @see org.sonar.process.test.HttpProcess + */ + void kill() { + try { + HttpRequest.post("http://localhost:" + httpPort + "/kill") + .readTimeout(500).connectTimeout(500).ok(); + } catch (Exception e) { + // HTTP request can't be fully processed, as web server hardly + // calls "System.exit()" + } + } + + /** + * @see org.sonar.process.test.HttpProcess + */ + boolean wasGracefullyTerminated() { + return fileExists("terminatedAt"); + } + + long wasStartingAt() throws IOException { + return readTimeFromFile("startingAt"); + } + + long wasGracefullyTerminatedAt() throws IOException { + return readTimeFromFile("terminatedAt"); + } + + boolean wasReady() throws IOException { + return fileExists("readyAt"); + } + + long wasReadyAt() throws IOException { + return readTimeFromFile("readyAt"); + } + + private long readTimeFromFile(String filename) throws IOException { + File file = new File(tempDir, filename); + if (file.isFile() && file.exists()) { + return Long.parseLong(FileUtils.readFileToString(file)); + } + throw new IllegalStateException("File does not exist"); + } + + private boolean fileExists(String filename) { + File file = new File(tempDir, filename); + return file.isFile() && file.exists(); + } + } + + private JavaCommand newStandardProcessCommand() throws IOException { + return new JavaCommand("standard") + .addClasspath(testJar.getAbsolutePath()) + .setClassName("org.sonar.process.test.StandardProcess") + .setJmxPort(NetworkUtils.freePort()) + .setTempDir(temp.newFolder()); + } + +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java new file mode 100644 index 00000000000..9fb9405f21d --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java @@ -0,0 +1,31 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +public class TerminationFailureRmiConnector extends RmiJmxConnector { + TerminationFailureRmiConnector(Timeouts timeouts) { + super(timeouts); + } + + @Override + public void terminate(ProcessRef processRef) { + throw new IllegalStateException("Test - fail to send termination request"); + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java new file mode 100644 index 00000000000..5a1d5b590d4 --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java @@ -0,0 +1,53 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.junit.Test; + +import static org.fest.assertions.Assertions.assertThat; + +public class TimeoutsTest { + + @Test + public void test_default_values() throws Exception { + Timeouts timeouts = new Timeouts(); + assertThat(timeouts.getMonitorPingInterval()).isGreaterThan(1000L); + assertThat(timeouts.getAutokillPingInterval()).isGreaterThan(1000L); + assertThat(timeouts.getAutokillPingTimeout()).isGreaterThan(1000L); + assertThat(timeouts.getTerminationTimeout()).isGreaterThan(1000L); + assertThat(timeouts.getJmxConnectionTimeout()).isGreaterThan(1000L); + } + + @Test + public void test_values() throws Exception { + Timeouts timeouts = new Timeouts(); + timeouts.setAutokillPingInterval(1L); + timeouts.setAutokillPingTimeout(2L); + timeouts.setTerminationTimeout(3L); + timeouts.setJmxConnectionTimeout(4L); + timeouts.setMonitorPingInterval(5L); + + assertThat(timeouts.getAutokillPingInterval()).isEqualTo(1L); + assertThat(timeouts.getAutokillPingTimeout()).isEqualTo(2L); + assertThat(timeouts.getTerminationTimeout()).isEqualTo(3L); + assertThat(timeouts.getJmxConnectionTimeout()).isEqualTo(4L); + assertThat(timeouts.getMonitorPingInterval()).isEqualTo(5L); + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java new file mode 100644 index 00000000000..86f3a47e6ff --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java @@ -0,0 +1,52 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class WatcherThreadTest { + + @Test(timeout = 10000L) + public void kill_process_if_watcher_is_interrupted() throws Exception { + ProcessRef ref = mock(ProcessRef.class, Mockito.RETURNS_DEEP_STUBS); + when(ref.getProcess().waitFor()).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocationOnMock) throws Throwable { + Thread.sleep(Long.MAX_VALUE); + return 0; + } + }); + Monitor monitor = mock(Monitor.class); + + WatcherThread watcher = new WatcherThread(ref, monitor); + watcher.start(); + Thread.sleep(50L); + watcher.interrupt(); + + verify(ref).hardKill(); + } +} diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt new file mode 100644 index 00000000000..65b98c522da --- /dev/null +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt @@ -0,0 +1 @@ +0PZz+G+f8mjr3sPn4+AhHg== \ No newline at end of file diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt new file mode 100644 index 00000000000..b33e179e5c8 --- /dev/null +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt @@ -0,0 +1 @@ +badbadbad== \ No newline at end of file diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt new file mode 100644 index 00000000000..ab83e4adc03 --- /dev/null +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt @@ -0,0 +1,3 @@ + + 0PZz+G+f8mjr3sPn4+AhHg== + diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt new file mode 100644 index 00000000000..23f5ecf5104 --- /dev/null +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt @@ -0,0 +1 @@ +IBxEUxZ41c8XTxyaah1Qlg== \ No newline at end of file diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml b/server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml new file mode 100644 index 00000000000..298193e01fa --- /dev/null +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml @@ -0,0 +1 @@ + diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties b/server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties new file mode 100644 index 00000000000..1577a214b3b --- /dev/null +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties @@ -0,0 +1,212 @@ +# This file must contain only ISO 8859-1 characters +# see http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Properties.html#load(java.io.InputStream) +# +# To use an environment variable, use the following syntax : ${env:NAME_OF_ENV_VARIABLE} +# For example: +# sonar.jdbc.url= ${env:SONAR_JDBC_URL} +# +# +# See also the file conf/wrapper.conf for JVM advanced settings + + + +#-------------------------------------------------------------------------------------------------- +# DATABASE +# +# IMPORTANT: the embedded H2 database is used by default. It is recommended for tests only. +# Please use a production-ready database. Supported databases are MySQL, Oracle, PostgreSQL +# and Microsoft SQLServer. + +# Permissions to create tables, indices and triggers must be granted to JDBC user. +# The schema must be created first. +sonar.jdbc.username=sonar +sonar.jdbc.password=sonar + +#----- Embedded database H2 +# Note: it does not accept connections from remote hosts, so the +# SonarQube server and the maven plugin must be executed on the same host. + +# Comment the following line to deactivate the default embedded database. +sonar.jdbc.url=jdbc:h2:tcp://localhost:9092/sonar + +# directory containing H2 database files. By default it's the /data directory in the SonarQube installation. +#sonar.embeddedDatabase.dataDir= +# H2 embedded database server listening port, defaults to 9092 +#sonar.embeddedDatabase.port=9092 + + +#----- MySQL 5.x +# Comment the embedded database and uncomment the following line to use MySQL +#sonar.jdbc.url=jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true + + +#----- Oracle 10g/11g +# To connect to Oracle database: +# +# - It's recommended to use the latest version of the JDBC driver (ojdbc6.jar). +# Download it in http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html +# - Copy the driver to the directory extensions/jdbc-driver/oracle/ +# - If you need to set the schema, please refer to http://jira.codehaus.org/browse/SONAR-5000 +# - Comment the embedded database and uncomment the following line: +#sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE + + +#----- PostgreSQL 8.x/9.x +# Comment the embedded database and uncomment the following property to use PostgreSQL. +# If you don't use the schema named "public", please refer to http://jira.codehaus.org/browse/SONAR-5000 +#sonar.jdbc.url=jdbc:postgresql://localhost/sonar + + +#----- Microsoft SQLServer +# The Jtds open source driver is available in extensions/jdbc-driver/mssql. More details on http://jtds.sourceforge.net +#sonar.jdbc.url=jdbc:jtds:sqlserver://localhost/sonar;SelectMethod=Cursor + + +#----- Connection pool settings +sonar.jdbc.maxActive=20 +sonar.jdbc.maxIdle=5 +sonar.jdbc.minIdle=2 +sonar.jdbc.maxWait=5000 +sonar.jdbc.minEvictableIdleTimeMillis=600000 +sonar.jdbc.timeBetweenEvictionRunsMillis=30000 + + + +#-------------------------------------------------------------------------------------------------- +# WEB SERVER + +# Binding IP address. For servers with more than one IP address, this property specifies which +# address will be used for listening on the specified ports. +# By default, ports will be used on all IP addresses associated with the server. +#sonar.web.host=0.0.0.0 + +# Web context. When set, it must start with forward slash (for example /sonarqube). +# The default value is root context (empty value). +#sonar.web.context= + +# TCP port for incoming HTTP connections. Disabled when value is -1. +#sonar.web.port=9000 + +# TCP port for incoming HTTPS connections. Disabled when value is -1 (default). +#sonar.web.https.port=-1 + +# HTTPS - the alias used to for the server certificate in the keystore. +# If not specified the first key read in the keystore is used. +#sonar.web.https.keyAlias= + +# HTTPS - the password used to access the server certificate from the +# specified keystore file. The default value is "changeit". +#sonar.web.https.keyPass=changeit + +# HTTPS - the pathname of the keystore file where is stored the server certificate. +# By default, the pathname is the file ".keystore" in the user home. +# If keystoreType doesn't need a file use empty value. +#sonar.web.https.keystoreFile= + +# HTTPS - the password used to access the specified keystore file. The default +# value is the value of sonar.web.https.keyPass. +#sonar.web.https.keystorePass= + +# HTTPS - the type of keystore file to be used for the server certificate. +# The default value is JKS (Java KeyStore). +#sonar.web.https.keystoreType=JKS + +# HTTPS - the name of the keystore provider to be used for the server certificate. +# If not specified, the list of registered providers is traversed in preference order +# and the first provider that supports the keystore type is used (see sonar.web.https.keystoreType). +#sonar.web.https.keystoreProvider= + +# HTTPS - the pathname of the truststore file which contains trusted certificate authorities. +# By default, this would be the cacerts file in your JRE. +# If truststoreFile doesn't need a file use empty value. +#sonar.web.https.truststoreFile= + +# HTTPS - the password used to access the specified truststore file. +#sonar.web.https.truststorePass= + +# HTTPS - the type of truststore file to be used. +# The default value is JKS (Java KeyStore). +#sonar.web.https.truststoreType=JKS + +# HTTPS - the name of the truststore provider to be used for the server certificate. +# If not specified, the list of registered providers is traversed in preference order +# and the first provider that supports the truststore type is used (see sonar.web.https.truststoreType). +#sonar.web.https.truststoreProvider= + +# HTTPS - whether to enable client certificate authentication. +# The default is false (client certificates disabled). +# Other possible values are 'want' (certificates will be requested, but not required), +# and 'true' (certificates are required). +#sonar.web.https.clientAuth=false + +# The maximum number of connections that the server will accept and process at any given time. +# When this number has been reached, the server will not accept any more connections until +# the number of connections falls below this value. The operating system may still accept connections +# based on the sonar.web.connections.acceptCount property. The default value is 50 for each +# enabled connector. +#sonar.web.http.maxThreads=50 +#sonar.web.https.maxThreads=50 + +# The minimum number of threads always kept running. The default value is 5 for each +# enabled connector. +#sonar.web.http.minThreads=5 +#sonar.web.https.minThreads=5 + +# The maximum queue length for incoming connection requests when all possible request processing +# threads are in use. Any requests received when the queue is full will be refused. +# The default value is 25 for each enabled connector. +#sonar.web.http.acceptCount=25 +#sonar.web.https.acceptCount=25 + +# Access logs are generated in the file logs/access.log. This file is rolled over when it's 5Mb. +# An archive of 3 files is kept in the same directory. +# Access logs are enabled by default. +#sonar.web.accessLogs.enable=true + +# TCP port for incoming AJP connections. Disabled when value is -1. +# sonar.ajp.port=9009 + + + +#-------------------------------------------------------------------------------------------------- +# UPDATE CENTER + +# The Update Center requires an internet connection to request http://update.sonarsource.org +# It is enabled by default. +#sonar.updatecenter.activate=true + +# HTTP proxy (default none) +#http.proxyHost= +#http.proxyPort= + +# NT domain name if NTLM proxy is used +#http.auth.ntlm.domain= + +# SOCKS proxy (default none) +#socksProxyHost= +#socksProxyPort= + +# proxy authentication. The 2 following properties are used for HTTP and SOCKS proxies. +#http.proxyUser= +#http.proxyPassword= + + +#-------------------------------------------------------------------------------------------------- +# NOTIFICATIONS + +# Delay in seconds between processing of notification queue. Default is 60. +#sonar.notifications.delay=60 + + +#-------------------------------------------------------------------------------------------------- +# PROFILING +# Level of information displayed in the logs: NONE (default), BASIC (functional information) and FULL (functional and technical details) +#sonar.log.profilingLevel=NONE + + +#-------------------------------------------------------------------------------------------------- +# DEVELOPMENT MODE +# Only for debugging + +# Set to true to apply Ruby on Rails code changes on the fly +#sonar.rails.dev=false diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties b/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties new file mode 100644 index 00000000000..5c06e58a32e --- /dev/null +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties @@ -0,0 +1,3 @@ +hello: world +foo=bar +java.io.tmpdir=/should/be/overridden diff --git a/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar b/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar new file mode 100644 index 00000000000..6dfd458329a Binary files /dev/null and b/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar differ diff --git a/server/sonar-process/pom.xml b/server/sonar-process/pom.xml new file mode 100644 index 00000000000..25413fd7ce9 --- /dev/null +++ b/server/sonar-process/pom.xml @@ -0,0 +1,108 @@ + + + + org.codehaus.sonar + server + 4.5-SNAPSHOT + ../ + + 4.0.0 + + sonar-process + SonarQube :: Process + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + commons-codec + commons-codec + + + commons-io + commons-io + + + commons-lang + commons-lang + + + com.google.code.findbugs + jsr305 + provided + + + + junit + junit + test + + + org.easytesting + fest-assert + test + + + org.mockito + mockito-core + test + + + org.hamcrest + hamcrest-all + test + + + com.google.guava + guava + test + + + org.eclipse.jetty + jetty-server + test + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + test-jar-with-dependencies.xml + + + + + package + + single + + + + + + + diff --git a/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java b/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java new file mode 100644 index 00000000000..204ae1a6b6b --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java @@ -0,0 +1,133 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.process; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; + +import javax.annotation.Nullable; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.File; +import java.io.IOException; +import java.security.Key; +import java.security.SecureRandom; + +final class AesCipher implements Cipher { + + // Can't be increased because of Java 6 policy files : + // https://confluence.terena.org/display/~visser/No+256+bit+ciphers+for+Java+apps + // http://java.sun.com/javase/6/webnotes/install/jre/README + public static final int KEY_SIZE_IN_BITS = 128; + + private static final String CRYPTO_KEY = "AES"; + + /** + * Duplication from CoreProperties.ENCRYPTION_SECRET_KEY_PATH + */ + static final String ENCRYPTION_SECRET_KEY_PATH = "sonar.secretKeyPath"; + + private String pathToSecretKey; + + AesCipher(@Nullable String pathToSecretKey) { + this.pathToSecretKey = pathToSecretKey; + } + + @Override + public String encrypt(String clearText) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY); + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, loadSecretFile()); + return new String(Base64.encodeBase64(cipher.doFinal(clearText.getBytes("UTF-8")))); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public String decrypt(String encryptedText) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY); + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, loadSecretFile()); + byte[] cipherData = cipher.doFinal(Base64.decodeBase64(StringUtils.trim(encryptedText))); + return new String(cipherData); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + /** + * This method checks the existence of the file, but not the validity of the contained key. + */ + boolean hasSecretKey() { + String path = getPathToSecretKey(); + if (StringUtils.isNotBlank(path)) { + File file = new File(path); + return file.exists() && file.isFile(); + } + return false; + } + + private Key loadSecretFile() throws IOException { + String path = getPathToSecretKey(); + return loadSecretFileFromFile(path); + } + + Key loadSecretFileFromFile(String path) throws IOException { + if (StringUtils.isBlank(path)) { + throw new IllegalStateException("Secret key not found. Please set the property " + ENCRYPTION_SECRET_KEY_PATH); + } + File file = new File(path); + if (!file.exists() || !file.isFile()) { + throw new IllegalStateException("The property " + ENCRYPTION_SECRET_KEY_PATH + " does not link to a valid file: " + path); + } + String s = FileUtils.readFileToString(file); + if (StringUtils.isBlank(s)) { + throw new IllegalStateException("No secret key in the file: " + path); + } + return new SecretKeySpec(Base64.decodeBase64(StringUtils.trim(s)), CRYPTO_KEY); + } + + String generateRandomSecretKey() { + try { + KeyGenerator keyGen = KeyGenerator.getInstance(CRYPTO_KEY); + keyGen.init(KEY_SIZE_IN_BITS, new SecureRandom()); + SecretKey secretKey = keyGen.generateKey(); + return new String(Base64.encodeBase64(secretKey.getEncoded())); + + } catch (Exception e) { + throw new IllegalStateException("Fail to generate secret key", e); + } + } + + String getPathToSecretKey() { + if (StringUtils.isBlank(pathToSecretKey)) { + pathToSecretKey = new File(FileUtils.getUserDirectoryPath(), ".sonar/sonar-secret.txt").getPath(); + } + return pathToSecretKey; + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java b/server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java new file mode 100644 index 00000000000..5eb3eecd541 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java @@ -0,0 +1,35 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.process; + +import org.apache.commons.codec.binary.Base64; + +final class Base64Cipher implements Cipher { + @Override + public String encrypt(String clearText) { + return new String(Base64.encodeBase64(clearText.getBytes())); + } + + @Override + public String decrypt(String encryptedText) { + return new String(Base64.decodeBase64(encryptedText)); + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/Cipher.java b/server/sonar-process/src/main/java/org/sonar/process/Cipher.java new file mode 100644 index 00000000000..4c437057757 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/Cipher.java @@ -0,0 +1,27 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.process; + +interface Cipher { + String encrypt(String clearText); + + String decrypt(String encryptedText); +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java b/server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java new file mode 100644 index 00000000000..b4f86457555 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java @@ -0,0 +1,70 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.text.StrSubstitutor; + +import java.io.File; +import java.io.FileReader; +import java.util.Enumeration; +import java.util.Map; +import java.util.Properties; + +public final class ConfigurationUtils { + + private ConfigurationUtils() { + // Utility class + } + + public static Properties interpolateVariables(Properties properties, Map variables) { + Properties result = new Properties(); + Enumeration keys = properties.keys(); + while (keys.hasMoreElements()) { + String key = (String) keys.nextElement(); + String value = (String) properties.get(key); + String interpolatedValue = StrSubstitutor.replace(value, variables, "${env:", "}"); + result.setProperty(key, interpolatedValue); + } + return result; + } + + static Props loadPropsFromCommandLineArgs(String[] args) { + if (args.length != 1) { + throw new IllegalArgumentException("Only a single command-line argument is accepted " + + "(absolute path to configuration file)"); + } + + File propertyFile = new File(args[0]); + Properties properties = new Properties(); + FileReader reader = null; + try { + reader = new FileReader(propertyFile); + properties.load(reader); + } catch (Exception e) { + throw new IllegalStateException("Could not read properties from file: " + args[0], e); + } finally { + IOUtils.closeQuietly(reader); + FileUtils.deleteQuietly(propertyFile); + } + return new Props(properties); + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/Encryption.java b/server/sonar-process/src/main/java/org/sonar/process/Encryption.java new file mode 100644 index 00000000000..cca05e6c780 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/Encryption.java @@ -0,0 +1,64 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.process; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @since 3.0 + */ +public final class Encryption { + + private static final String BASE64_ALGORITHM = "b64"; + + private static final String AES_ALGORITHM = "aes"; + private final AesCipher aesCipher; + + private final Map ciphers = new HashMap(); + private static final Pattern ENCRYPTED_PATTERN = Pattern.compile("\\{(.*?)\\}(.*)"); + + public Encryption(@Nullable String pathToSecretKey) { + aesCipher = new AesCipher(pathToSecretKey); + ciphers.put(BASE64_ALGORITHM, new Base64Cipher()); + ciphers.put(AES_ALGORITHM, aesCipher); + } + + public boolean isEncrypted(String value) { + return value.indexOf('{') == 0 && value.indexOf('}') > 1; + } + + public String decrypt(String encryptedText) { + Matcher matcher = ENCRYPTED_PATTERN.matcher(encryptedText); + if (matcher.matches()) { + Cipher cipher = ciphers.get(matcher.group(1).toLowerCase(Locale.ENGLISH)); + if (cipher != null) { + return cipher.decrypt(matcher.group(2)); + } + } + return encryptedText; + } + +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java b/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java new file mode 100644 index 00000000000..c0cf02bcc89 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java @@ -0,0 +1,81 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.remote.JMXServiceURL; + +import java.lang.management.ManagementFactory; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.MalformedURLException; + +public class JmxUtils { + + private JmxUtils() { + // only static stuff + } + + public static final String DOMAIN = "org.sonar"; + public static final String NAME_PROPERTY = "name"; + + public static final String WEB_SERVER_NAME = "web"; + public static final String SEARCH_SERVER_NAME = "search"; + + public static ObjectName objectName(String name) { + try { + return new ObjectName(DOMAIN, NAME_PROPERTY, name); + } catch (MalformedObjectNameException e) { + throw new IllegalStateException("Cannot create ObjectName for " + name, e); + } + } + + public static void registerMBean(Object mbean, String name) { + try { + MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); + ObjectName oName = objectName(name); + // Check if already registered in JVM (might run multiple instance in JUnits) + if (mbeanServer.isRegistered(oName)) { + mbeanServer.unregisterMBean(oName); + } + mbeanServer.registerMBean(mbean, oName); + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new IllegalStateException("Fail to register JMX MBean named " + name, e); + } + } + + public static JMXServiceURL serviceUrl(InetAddress host, int port) { + String address = host.getHostAddress(); + if (host instanceof Inet6Address) { + // See http://docs.oracle.com/javase/7/docs/api/javax/management/remote/JMXServiceURL.html + // "The host is a host name, an IPv4 numeric host address, or an IPv6 numeric address enclosed in square brackets." + address = String.format("[%s]", address); + } + try { + return new JMXServiceURL("rmi", address, port, String.format("/jndi/rmi://%s:%d/jmxrmi", address, port)); + } catch (MalformedURLException e) { + throw new IllegalStateException("JMX url does not look well formed", e); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java b/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java new file mode 100644 index 00000000000..a492ac751ca --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java @@ -0,0 +1,56 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +public class Lifecycle { + + private State state = State.INIT; + + public State getState() { + return state; + } + + public synchronized boolean tryToMoveTo(State to) { + if (state.ordinal() < to.ordinal()) { + state = to; + return true; + } + return false; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Lifecycle lifecycle = (Lifecycle) o; + return state == lifecycle.state; + } + + @Override + public int hashCode() { + return state.hashCode(); + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java b/server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java new file mode 100644 index 00000000000..2302a626d2c --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java @@ -0,0 +1,71 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; + +public class LoopbackAddress { + + private static InetAddress instance; + + private LoopbackAddress() { + // only static stuff + } + + /** + * Quite similar to {@code InetAddress.getLoopbackAddress()} which was introduced in Java 7. This + * method aims to support Java 6. It returns an IPv4 address, but not IPv6 in order to + * support {@code -Djava.net.preferIPv4Stack=true} which is recommended for Elasticsearch. + */ + public static InetAddress get() { + if (instance == null) { + try { + instance = doGet(NetworkInterface.getNetworkInterfaces()); + } catch (SocketException e) { + throw new IllegalStateException("Fail to browse network interfaces", e); + } + + } + return instance; + } + + static InetAddress doGet(Enumeration ifaces) { + InetAddress result = null; + while (ifaces.hasMoreElements() && result == null) { + NetworkInterface iface = ifaces.nextElement(); + Enumeration addresses = iface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (addr.isLoopbackAddress() && addr instanceof Inet4Address) { + result = addr; + break; + } + } + } + if (result == null) { + throw new IllegalStateException("Impossible to get a IPv4 loopback address"); + } + return result; + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/MessageException.java b/server/sonar-process/src/main/java/org/sonar/process/MessageException.java new file mode 100644 index 00000000000..5b86ef66c64 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/MessageException.java @@ -0,0 +1,36 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +public class MessageException extends RuntimeException { + public MessageException(String message) { + super(message); + } + + /** + * Does not fill in the stack trace + * + * @see Throwable#fillInStackTrace() + */ + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java b/server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java new file mode 100644 index 00000000000..2389fa5aa18 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java @@ -0,0 +1,86 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class MinimumViableSystem { + + private final Map requiredJavaOptions = new HashMap(); + + public MinimumViableSystem setRequiredJavaOption(String propertyKey, String expectedValue) { + requiredJavaOptions.put(propertyKey, expectedValue); + return this; + } + + /** + * Entry point for all checks + */ + public void check() { + checkJavaVersion(); + checkJavaOptions(); + checkWritableTempDir(); + } + + /** + * Verify that temp directory is writable + */ + private void checkWritableTempDir() { + checkWritableDir(System.getProperty("java.io.tmpdir")); + } + + void checkWritableDir(String tempPath) { + try { + File tempFile = File.createTempFile("check", "tmp", new File(tempPath)); + FileUtils.deleteQuietly(tempFile); + } catch (IOException e) { + throw new IllegalStateException(String.format("Temp directory is not writable: %s", tempPath), e); + } + } + + void checkJavaOptions() { + for (Map.Entry entry : requiredJavaOptions.entrySet()) { + String value = System.getProperty(entry.getKey()); + if (!StringUtils.equals(value, entry.getValue())) { + throw new MessageException(String.format( + "JVM option '%s' must be set to '%s'. Got '%s'", entry.getKey(), entry.getValue(), StringUtils.defaultString(value))); + } + } + } + + void checkJavaVersion() { + String javaVersion = System.getProperty("java.specification.version"); + checkJavaVersion(javaVersion); + } + + void checkJavaVersion(String javaVersion) { + if (!javaVersion.startsWith("1.6") && !javaVersion.startsWith("1.7") && !javaVersion.startsWith("1.8")) { + // still better than "java.lang.UnsupportedClassVersionError: Unsupported major.minor version 49.0 + throw new MessageException(String.format("Supported versions of Java are 1.6, 1.7 and 1.8. Got %s.", javaVersion)); + } + } + +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java b/server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java new file mode 100644 index 00000000000..6ee84d00744 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java @@ -0,0 +1,31 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +public interface MonitoredProcess extends Terminable { + + /** + * Starts and blocks until ready + */ + void start(); + + void awaitTermination(); + +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java b/server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java new file mode 100644 index 00000000000..074cb8cf5c2 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java @@ -0,0 +1,40 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import java.net.ServerSocket; + +public class NetworkUtils { + + private NetworkUtils() { + // only static stuff + } + + public static int freePort() { + try { + ServerSocket s = new ServerSocket(0); + int port = s.getLocalPort(); + s.close(); + return port; + } catch (Exception e) { + throw new IllegalStateException("Can not find an open network port", e); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java new file mode 100644 index 00000000000..79ec79f74b7 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java @@ -0,0 +1,148 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ProcessEntryPoint implements ProcessMXBean { + + public static final String PROPERTY_PROCESS_KEY = "process.key"; + public static final String PROPERTY_AUTOKILL_DISABLED = "process.autokill.disabled"; + public static final String PROPERTY_AUTOKILL_PING_TIMEOUT = "process.autokill.pingTimeout"; + public static final String PROPERTY_AUTOKILL_PING_INTERVAL = "process.autokill.pingInterval"; + public static final String PROPERTY_TERMINATION_TIMEOUT = "process.terminationTimeout"; + + private final Props props; + private final Lifecycle lifecycle = new Lifecycle(); + private volatile MonitoredProcess monitoredProcess; + private volatile long lastPing = 0L; + private volatile StopperThread stopperThread; + private final SystemExit exit; + private Thread shutdownHook = new Thread(new Runnable() { + @Override + public void run() { + exit.setInShutdownHook(); + terminate(); + } + }); + + ProcessEntryPoint(Props props, SystemExit exit) { + this.props = props; + this.exit = exit; + } + + public Props getProps() { + return props; + } + + /** + * Launch process and waits until it's down + */ + public void launch(MonitoredProcess mp) { + if (!lifecycle.tryToMoveTo(State.STARTING)) { + throw new IllegalStateException("Already started"); + } + monitoredProcess = mp; + + // TODO check if these properties are available in System Info + JmxUtils.registerMBean(this, props.nonNullValue(PROPERTY_PROCESS_KEY)); + Runtime.getRuntime().addShutdownHook(shutdownHook); + if (!props.valueAsBoolean(PROPERTY_AUTOKILL_DISABLED, false)) { + // mainly for Java Debugger + scheduleAutokill(); + } + + try { + monitoredProcess.start(); + if (lifecycle.tryToMoveTo(State.STARTED)) { + monitoredProcess.awaitTermination(); + } + } catch (Exception ignored) { + } finally { + terminate(); + } + } + + @Override + public boolean isReady() { + return lifecycle.getState() == State.STARTED; + } + + @Override + public void ping() { + lastPing = System.currentTimeMillis(); + } + + /** + * Blocks until stopped in a timely fashion (see {@link org.sonar.process.StopperThread}) + */ + @Override + public void terminate() { + if (lifecycle.tryToMoveTo(State.STOPPING)) { + stopperThread = new StopperThread(monitoredProcess, Long.parseLong(props.nonNullValue(PROPERTY_TERMINATION_TIMEOUT))); + stopperThread.start(); + } + try { + // stopperThread is not null for sure + // join() does nothing if thread already finished + stopperThread.join(); + lifecycle.tryToMoveTo(State.STOPPED); + } catch (InterruptedException e) { + // nothing to do, the process is going to be exited + } + exit.exit(0); + } + + private void scheduleAutokill() { + final long autokillPingTimeoutMs = props.valueAsInt(PROPERTY_AUTOKILL_PING_TIMEOUT); + long autokillPingIntervalMs = props.valueAsInt(PROPERTY_AUTOKILL_PING_INTERVAL); + Runnable autokiller = new Runnable() { + @Override + public void run() { + long time = System.currentTimeMillis(); + if (time - lastPing > autokillPingTimeoutMs) { + LoggerFactory.getLogger(getClass()).info(String.format( + "Did not receive any ping during %d seconds. Shutting down.", autokillPingTimeoutMs / 1000)); + terminate(); + } + } + }; + lastPing = System.currentTimeMillis(); + ScheduledExecutorService monitor = Executors.newScheduledThreadPool(1); + monitor.scheduleWithFixedDelay(autokiller, autokillPingIntervalMs, autokillPingIntervalMs, TimeUnit.MILLISECONDS); + } + + State getState() { + return lifecycle.getState(); + } + + Thread getShutdownHook() { + return shutdownHook; + } + + public static ProcessEntryPoint createForArguments(String[] args) { + Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args); + return new ProcessEntryPoint(props, new SystemExit()); + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java new file mode 100644 index 00000000000..dacddd91847 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java @@ -0,0 +1,53 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.joran.JoranConfigurator; +import ch.qos.logback.core.joran.spi.JoranException; +import ch.qos.logback.core.util.StatusPrinter; +import org.slf4j.LoggerFactory; + +public class ProcessLogging { + + private static final String PATH_LOGS_PROPERTY = "sonar.path.logs"; + + public void configure(Props props, String logbackXmlResource) { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + try { + JoranConfigurator configurator = new JoranConfigurator(); + configurator.setContext(context); + context.reset(); + context.putProperty(PATH_LOGS_PROPERTY, props.nonNullValue(PATH_LOGS_PROPERTY)); + doConfigure(configurator, logbackXmlResource); + } catch (JoranException ignored) { + // StatusPrinter will handle this + } + StatusPrinter.printInCaseOfErrorsOrWarnings(context); + + } + + /** + * Extracted only for unit testing + */ + void doConfigure(JoranConfigurator configurator, String logbackXmlResource) throws JoranException { + configurator.doConfigure(getClass().getResource(logbackXmlResource)); + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java new file mode 100644 index 00000000000..3d024d420eb --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java @@ -0,0 +1,28 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +public interface ProcessMXBean extends Terminable { + + boolean isReady(); + + void ping(); + +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java new file mode 100644 index 00000000000..bdefa116949 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java @@ -0,0 +1,77 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.apache.commons.io.IOUtils; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +public class ProcessUtils { + + private ProcessUtils() { + // only static stuff + } + + /** + * Do not abuse to this method. It uses exceptions to get status. + * @return false if process is null or terminated, else true. + */ + public static boolean isAlive(@Nullable Process process) { + boolean alive = false; + if (process != null) { + try { + process.exitValue(); + } catch (IllegalThreadStateException ignored) { + alive = true; + } + } + return alive; + } + + /** + * Destroys process (equivalent to kill -9) if alive + * @return true if the process was destroyed, false if process is null or already destroyed. + */ + public static boolean destroyQuietly(@Nullable Process process) { + boolean destroyed = false; + if (isAlive(process)) { + try { + process.destroy(); + while (isAlive(process)) { + // destroy() sends the signal, it does not wait for the process to be down + Thread.sleep(100L); + } + destroyed = true; + } catch (Exception e) { + LoggerFactory.getLogger(ProcessUtils.class).error("Fail to destroy " + process); + } + } + return destroyed; + } + + public static void closeStreams(@Nullable Process process) { + if (process != null) { + IOUtils.closeQuietly(process.getInputStream()); + IOUtils.closeQuietly(process.getOutputStream()); + IOUtils.closeQuietly(process.getErrorStream()); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/Props.java b/server/sonar-process/src/main/java/org/sonar/process/Props.java new file mode 100644 index 00000000000..b868702eafc --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/Props.java @@ -0,0 +1,120 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.apache.commons.lang.StringUtils; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import java.io.File; +import java.util.Properties; + +public class Props { + + private final Properties properties; + private final Encryption encryption; + + public Props(Properties props) { + this.properties = props; + this.encryption = new Encryption(props.getProperty(AesCipher.ENCRYPTION_SECRET_KEY_PATH)); + } + + public boolean contains(String key) { + return properties.containsKey(key); + } + + @CheckForNull + public String value(String key) { + String value = properties.getProperty(key); + if (value != null && encryption.isEncrypted(value)) { + value = encryption.decrypt(value); + } + return value; + } + + public String nonNullValue(String key) { + String value = value(key); + if (value == null) { + throw new IllegalArgumentException("Missing property: " + key); + } + return value; + } + + @CheckForNull + public String value(String key, @Nullable String defaultValue) { + String s = value(key); + return s == null ? defaultValue : s; + } + + public boolean valueAsBoolean(String key) { + String s = value(key); + return s != null && Boolean.parseBoolean(s); + } + + public boolean valueAsBoolean(String key, boolean defaultValue) { + String s = value(key); + return s != null ? Boolean.parseBoolean(s) : defaultValue; + } + + public File nonNullValueAsFile(String key) { + String s = value(key); + if (s == null) { + throw new IllegalArgumentException("Property " + key + " is missing"); + } + return new File(s); + } + + @CheckForNull + public Integer valueAsInt(String key) { + String s = value(key); + if (s != null && !"".equals(s)) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + throw new IllegalStateException("Value of property " + key + " is not an integer: " + s, e); + } + } + return null; + } + + public int valueAsInt(String key, int defaultValue) { + Integer i = valueAsInt(key); + return i == null ? defaultValue : i; + } + + public Properties rawProperties() { + return properties; + } + + public Props set(String key, @Nullable String value) { + if (value != null) { + properties.setProperty(key, value); + } + return this; + } + + public void setDefault(String key, String value) { + String s = properties.getProperty(key); + if (StringUtils.isBlank(s)) { + properties.setProperty(key, value); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/State.java b/server/sonar-process/src/main/java/org/sonar/process/State.java new file mode 100644 index 00000000000..9d773d2bc23 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/State.java @@ -0,0 +1,26 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +public enum State { + + INIT, STARTING, STARTED, STOPPING, STOPPED + +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/StopperThread.java b/server/sonar-process/src/main/java/org/sonar/process/StopperThread.java new file mode 100644 index 00000000000..2d0c6734b30 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/StopperThread.java @@ -0,0 +1,57 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * Gracefully stops process, but exits JVM if too long + */ +class StopperThread extends Thread { + + private final Terminable terminable; + private final long terminationTimeout; + + StopperThread(Terminable terminable, long terminationTimeout) { + super("Stopper"); + this.terminable = terminable; + this.terminationTimeout = terminationTimeout; + } + + @Override + public void run() { + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(new Runnable() { + @Override + public void run() { + terminable.terminate(); + } + }); + try { + future.get(terminationTimeout, TimeUnit.MILLISECONDS); + } catch (Exception e) { + future.cancel(true); + executor.shutdownNow(); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/SystemExit.java b/server/sonar-process/src/main/java/org/sonar/process/SystemExit.java new file mode 100644 index 00000000000..8f12a210633 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/SystemExit.java @@ -0,0 +1,52 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Calls {@link System#exit(int)} except from shutdown hooks, to prevent + * deadlocks. See http://stackoverflow.com/a/19552359/229031 + */ +public class SystemExit { + + private final AtomicBoolean inShutdownHook = new AtomicBoolean(false); + + public void exit(int code) { + if (!inShutdownHook.get()) { + doExit(code); + } + } + + public boolean isInShutdownHook() { + return inShutdownHook.get(); + } + + /** + * Declarative approach. I don't know how to get this lifecycle state from Java API. + */ + public void setInShutdownHook() { + inShutdownHook.set(true); + } + + void doExit(int code) { + System.exit(code); + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/Terminable.java b/server/sonar-process/src/main/java/org/sonar/process/Terminable.java new file mode 100644 index 00000000000..a8670609fe2 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/Terminable.java @@ -0,0 +1,28 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +/** + * This term "terminate" is used in order to not conflict with {@link Thread#stop()}. + */ +public interface Terminable { + + void terminate(); +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/package-info.java b/server/sonar-process/src/main/java/org/sonar/process/package-info.java new file mode 100644 index 00000000000..09da5ce266d --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.process; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java b/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java new file mode 100644 index 00000000000..8350eafaa3e --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java @@ -0,0 +1,185 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.process; + +import com.google.common.io.Resources; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import javax.crypto.BadPaddingException; +import java.io.File; +import java.security.InvalidKeyException; +import java.security.Key; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + + +public class AesCipherTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void generateRandomSecretKey() { + AesCipher cipher = new AesCipher(null); + + String key = cipher.generateRandomSecretKey(); + + assertThat(StringUtils.isNotBlank(key)).isTrue(); + assertThat(Base64.isArrayByteBase64(key.getBytes())).isTrue(); + } + + @Test + public void encrypt() throws Exception { + AesCipher cipher = new AesCipher(pathToSecretKey()); + + String encryptedText = cipher.encrypt("this is a secret"); + + assertThat(StringUtils.isNotBlank(encryptedText)).isTrue(); + assertThat(Base64.isArrayByteBase64(encryptedText.getBytes())).isTrue(); + } + + @Test + public void encrypt_bad_key() throws Exception { + thrown.expect(RuntimeException.class); + thrown.expectMessage("Invalid AES key"); + + AesCipher cipher = new AesCipher(getPath("bad_secret_key.txt")); + + cipher.encrypt("this is a secret"); + } + + @Test + public void decrypt() throws Exception { + AesCipher cipher = new AesCipher(pathToSecretKey()); + + // the following value has been encrypted with the key /org/sonar/api/config/AesCipherTest/aes_secret_key.txt + String clearText = cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY="); + + assertThat(clearText).isEqualTo("this is a secret"); + } + + @Test + public void decrypt_bad_key() throws Exception { + AesCipher cipher = new AesCipher(getPath("bad_secret_key.txt")); + + try { + cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY="); + fail(); + + } catch (RuntimeException e) { + assertThat(e.getCause()).isInstanceOf(InvalidKeyException.class); + } + } + + @Test + public void decrypt_other_key() throws Exception { + AesCipher cipher = new AesCipher(getPath("other_secret_key.txt")); + + try { + // text encrypted with another key + cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY="); + fail(); + + } catch (RuntimeException e) { + assertThat(e.getCause()).isInstanceOf(BadPaddingException.class); + } + } + + @Test + public void encryptThenDecrypt() throws Exception { + AesCipher cipher = new AesCipher(pathToSecretKey()); + + assertThat(cipher.decrypt(cipher.encrypt("foo"))).isEqualTo("foo"); + } + + @Test + public void testDefaultPathToSecretKey() { + AesCipher cipher = new AesCipher(null); + + String path = cipher.getPathToSecretKey(); + + assertThat(StringUtils.isNotBlank(path)).isTrue(); + assertThat(new File(path).getName()).isEqualTo("sonar-secret.txt"); + } + + @Test + public void loadSecretKeyFromFile() throws Exception { + AesCipher cipher = new AesCipher(null); + Key secretKey = cipher.loadSecretFileFromFile(pathToSecretKey()); + assertThat(secretKey.getAlgorithm()).isEqualTo("AES"); + assertThat(secretKey.getEncoded().length).isGreaterThan(10); + } + + @Test + public void loadSecretKeyFromFile_trim_content() throws Exception { + String path = getPath("non_trimmed_secret_key.txt"); + AesCipher cipher = new AesCipher(null); + + Key secretKey = cipher.loadSecretFileFromFile(path); + + assertThat(secretKey.getAlgorithm()).isEqualTo("AES"); + assertThat(secretKey.getEncoded().length).isGreaterThan(10); + } + + @Test + public void loadSecretKeyFromFile_file_does_not_exist() throws Exception { + thrown.expect(IllegalStateException.class); + + AesCipher cipher = new AesCipher(null); + cipher.loadSecretFileFromFile("/file/does/not/exist"); + } + + @Test + public void loadSecretKeyFromFile_no_property() throws Exception { + thrown.expect(IllegalStateException.class); + + AesCipher cipher = new AesCipher(null); + cipher.loadSecretFileFromFile(null); + } + + @Test + public void hasSecretKey() throws Exception { + AesCipher cipher = new AesCipher(pathToSecretKey()); + + assertThat(cipher.hasSecretKey()).isTrue(); + } + + @Test + public void doesNotHaveSecretKey() throws Exception { + AesCipher cipher = new AesCipher("/my/twitter/id/is/SimonBrandhof"); + + assertThat(cipher.hasSecretKey()).isFalse(); + } + + private static String getPath(String file) { + return Resources.getResource(AesCipherTest.class, "AesCipherTest/" + file).getPath(); + } + + private static String pathToSecretKey() throws Exception { + return getPath("aes_secret_key.txt"); + } + +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java b/server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java new file mode 100644 index 00000000000..2045cd4516d --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java @@ -0,0 +1,59 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.net.ServerSocket; + +public abstract class BaseProcessTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + public static final String DUMMY_OK_APP = "org.sonar.application.DummyOkProcess"; + + int freePort; + File dummyAppJar; + Process proc; + + @Before + public void setup() throws IOException { + ServerSocket socket = new ServerSocket(0); + freePort = socket.getLocalPort(); + socket.close(); + + dummyAppJar = FileUtils.toFile(getClass().getResource("/sonar-dummy-app.jar")); + } + + @After + public void tearDown() { + if (proc != null) { + ProcessUtils.destroyQuietly(proc); + } + } + +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java new file mode 100644 index 00000000000..de928b93850 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java @@ -0,0 +1,95 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import com.google.common.collect.Maps; +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.util.Map; +import java.util.Properties; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + +public class ConfigurationUtilsTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void shouldInterpolateVariables() { + Properties input = new Properties(); + input.setProperty("hello", "world"); + input.setProperty("url", "${env:SONAR_JDBC_URL}"); + input.setProperty("do_not_change", "${SONAR_JDBC_URL}"); + Map variables = Maps.newHashMap(); + variables.put("SONAR_JDBC_URL", "jdbc:h2:mem"); + + Properties output = ConfigurationUtils.interpolateVariables(input, variables); + + assertThat(output).hasSize(3); + assertThat(output.getProperty("hello")).isEqualTo("world"); + assertThat(output.getProperty("url")).isEqualTo("jdbc:h2:mem"); + assertThat(output.getProperty("do_not_change")).isEqualTo("${SONAR_JDBC_URL}"); + + // input is not changed + assertThat(input).hasSize(3); + assertThat(input.getProperty("hello")).isEqualTo("world"); + assertThat(input.getProperty("url")).isEqualTo("${env:SONAR_JDBC_URL}"); + assertThat(input.getProperty("do_not_change")).isEqualTo("${SONAR_JDBC_URL}"); + } + + @Test + public void loadPropsFromCommandLineArgs_missing_argument() throws Exception { + try { + ConfigurationUtils.loadPropsFromCommandLineArgs(new String[0]); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).startsWith("Only a single command-line argument is accepted"); + } + } + + @Test + public void loadPropsFromCommandLineArgs_load_properties_from_file() throws Exception { + File propsFile = temp.newFile(); + FileUtils.write(propsFile, "foo=bar"); + + Props result = ConfigurationUtils.loadPropsFromCommandLineArgs(new String[] {propsFile.getAbsolutePath()}); + assertThat(result.value("foo")).isEqualTo("bar"); + assertThat(result.rawProperties()).hasSize(1); + } + + @Test + public void loadPropsFromCommandLineArgs_file_does_not_exist() throws Exception { + File propsFile = temp.newFile(); + FileUtils.deleteQuietly(propsFile); + + try { + ConfigurationUtils.loadPropsFromCommandLineArgs(new String[]{propsFile.getAbsolutePath()}); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Could not read properties from file: " + propsFile.getAbsolutePath()); + } + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java b/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java new file mode 100644 index 00000000000..0c11856b0fa --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java @@ -0,0 +1,59 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.process; + +import org.junit.Test; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +public class EncryptionTest { + + @Test + public void isEncrypted() { + Encryption encryption = new Encryption(null); + assertThat(encryption.isEncrypted("{aes}ADASDASAD"), is(true)); + assertThat(encryption.isEncrypted("{b64}ADASDASAD"), is(true)); + assertThat(encryption.isEncrypted("{abc}ADASDASAD"), is(true)); + + assertThat(encryption.isEncrypted("{}"), is(false)); + assertThat(encryption.isEncrypted("{foo"), is(false)); + assertThat(encryption.isEncrypted("foo{aes}"), is(false)); + } + + @Test + public void decrypt() { + Encryption encryption = new Encryption(null); + assertThat(encryption.decrypt("{b64}Zm9v"), is("foo")); + } + + @Test + public void decrypt_unknown_algorithm() { + Encryption encryption = new Encryption(null); + assertThat(encryption.decrypt("{xxx}Zm9v"), is("{xxx}Zm9v")); + } + + @Test + public void decrypt_uncrypted_text() { + Encryption encryption = new Encryption(null); + assertThat(encryption.decrypt("foo"), is("foo")); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java new file mode 100644 index 00000000000..599ea5d7a30 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java @@ -0,0 +1,120 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.junit.Test; + +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.management.remote.JMXServiceURL; + +import java.lang.management.ManagementFactory; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Enumeration; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + +public class JmxUtilsTest { + + class MyBean implements ProcessMXBean { + + + @Override + public void terminate() { + + } + + @Override + public void ping() { + + } + + @Override + public boolean isReady() { + return true; + } + } + + @Test + public void construct_jmx_objectName() throws Exception { + MyBean mxBean = new MyBean(); + ObjectName objectName = JmxUtils.objectName(mxBean.getClass().getSimpleName()); + assertThat(objectName).isNotNull(); + assertThat(objectName.getDomain()).isEqualTo(JmxUtils.DOMAIN); + assertThat(objectName.getKeyProperty(JmxUtils.NAME_PROPERTY)).isEqualTo(mxBean.getClass().getSimpleName()); + } + + @Test + public void fail_jmx_objectName() throws Exception { + try { + JmxUtils.objectName(":"); + fail(); + } catch (Exception e) { + assertThat(e.getMessage()).isEqualTo("Cannot create ObjectName for :"); + } + } + + @Test + public void testRegisterMBean() throws Exception { + // 0 Get mbServer and create out test MXBean + MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); + MyBean mxBean = new MyBean(); + ObjectName objectName = JmxUtils.objectName(mxBean.getClass().getSimpleName()); + + // 1 assert that mxBean gets registered + assertThat(mbeanServer.isRegistered(objectName)).isFalse(); + JmxUtils.registerMBean(mxBean, mxBean.getClass().getSimpleName()); + assertThat(mbeanServer.isRegistered(objectName)).isTrue(); + } + + @Test + public void serviceUrl_ipv4() throws Exception { + JMXServiceURL url = JmxUtils.serviceUrl(ip(Inet4Address.class), 1234); + assertThat(url).isNotNull(); + assertThat(url.getPort()).isEqualTo(1234); + } + + @Test + public void serviceUrl_ipv6() throws Exception { + JMXServiceURL url = JmxUtils.serviceUrl(ip(Inet6Address.class), 1234); + assertThat(url).isNotNull(); + assertThat(url.getPort()).isEqualTo(1234); + } + + private static InetAddress ip(Class inetAddressClass) throws SocketException { + Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); + while (ifaces.hasMoreElements()) { + NetworkInterface iface = ifaces.nextElement(); + Enumeration addresses = iface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (addr.getClass().isAssignableFrom(inetAddressClass)) { + return addr; + } + } + } + throw new IllegalStateException("no ipv4 address"); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java b/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java new file mode 100644 index 00000000000..23886125212 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java @@ -0,0 +1,42 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.junit.Test; + +import static org.fest.assertions.Assertions.assertThat; + +public class LifecycleTest { + + @Test + public void equals_and_hashcode() throws Exception { + Lifecycle init = new Lifecycle(); + assertThat(init.equals(init)).isTrue(); + assertThat(init.equals(new Lifecycle())).isTrue(); + assertThat(init.equals("INIT")).isFalse(); + assertThat(init.equals(null)).isFalse(); + assertThat(init.hashCode()).isEqualTo(new Lifecycle().hashCode()); + + // different state + Lifecycle stopping = new Lifecycle(); + stopping.tryToMoveTo(State.STOPPING); + assertThat(stopping).isNotEqualTo(init); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java b/server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java new file mode 100644 index 00000000000..6a8819c3a81 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java @@ -0,0 +1,51 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import com.google.common.collect.Iterators; +import org.junit.Test; + +import java.net.NetworkInterface; +import java.util.Collections; +import java.util.Enumeration; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + +public class LoopbackAddressTest { + + @Test + public void get() throws Exception { + assertThat(LoopbackAddress.get()).isNotNull(); + assertThat(LoopbackAddress.get().isLoopbackAddress()).isTrue(); + assertThat(LoopbackAddress.get().getHostAddress()).isNotNull(); + } + + @Test + public void fail_to_get_loopback_address() throws Exception { + Enumeration ifaces = Iterators.asEnumeration(Collections.emptyList().iterator()); + try { + LoopbackAddress.doGet(ifaces); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Impossible to get a IPv4 loopback address"); + } + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java b/server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java new file mode 100644 index 00000000000..83841c79952 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java @@ -0,0 +1,102 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.fest.assertions.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; + +import static org.fest.assertions.Fail.fail; + +public class MinimumViableSystemTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + /** + * Verifies that all checks can be verified without error. + * Test environment does not necessarily follows all checks. + */ + @Test + public void check() throws Exception { + MinimumViableSystem mve = new MinimumViableSystem(); + + try { + mve.check(); + // ok + } catch (MessageException e) { + // also ok. All other exceptions are errors. + } + } + + @Test + public void checkJavaVersion() throws Exception { + MinimumViableSystem mve = new MinimumViableSystem(); + + // yes, sources are compiled with a supported Java version! + mve.checkJavaVersion(); + mve.checkJavaVersion("1.6"); + + try { + mve.checkJavaVersion("1.9"); + fail(); + } catch (MessageException e) { + Assertions.assertThat(e).hasMessage("Supported versions of Java are 1.6, 1.7 and 1.8. Got 1.9."); + } + } + + @Test + public void checkJavaOption() throws Exception { + String key = "MinimumViableEnvironmentTest.test.prop"; + MinimumViableSystem mve = new MinimumViableSystem() + .setRequiredJavaOption(key, "true"); + + try { + System.setProperty(key, "false"); + mve.checkJavaOptions(); + fail(); + } catch (MessageException e) { + Assertions.assertThat(e).hasMessage("JVM option '" + key + "' must be set to 'true'. Got 'false'"); + } + + System.setProperty(key, "true"); + mve.checkJavaOptions(); + // do not fail + } + + @Test + public void checkWritableTempDir() throws Exception { + File dir = temp.newFolder(); + MinimumViableSystem mve = new MinimumViableSystem(); + + mve.checkWritableDir(dir.getAbsolutePath()); + + dir.delete(); + try { + mve.checkWritableDir(dir.getAbsolutePath()); + fail(); + } catch (IllegalStateException e) { + Assertions.assertThat(e).hasMessage("Temp directory is not writable: " + dir.getAbsolutePath()); + } + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java new file mode 100644 index 00000000000..09f6a597209 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java @@ -0,0 +1,61 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.junit.Test; + +import java.net.ServerSocket; + +import static org.fest.assertions.Assertions.assertThat; + +public class NetworkUtilsTest { + + + @Test + public void find_free_port() throws Exception { + int port = NetworkUtils.freePort(); + assertThat(port).isGreaterThan(1024); + } + + @Test + public void find_multiple_free_port() throws Exception { + int port1 = NetworkUtils.freePort(); + int port2 = NetworkUtils.freePort(); + + assertThat(port1).isGreaterThan(1024); + assertThat(port2).isGreaterThan(1024); + + assertThat(port1).isNotSameAs(port2); + } + + @Test + public void find_multiple_free_non_adjacent_port() throws Exception { + int port1 = NetworkUtils.freePort(); + + ServerSocket socket = new ServerSocket(port1 + 1); + + int port2 = NetworkUtils.freePort(); + + assertThat(port1).isGreaterThan(1024); + assertThat(port2).isGreaterThan(1024); + + assertThat(port1).isNotSameAs(port2); + } +} \ No newline at end of file diff --git a/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java b/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java new file mode 100644 index 00000000000..55bf8d679d1 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java @@ -0,0 +1,224 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.Timeout; +import org.sonar.process.test.StandardProcess; + +import java.io.File; +import java.util.Properties; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Mockito.mock; + +public class ProcessEntryPointTest { + + SystemExit exit = mock(SystemExit.class); + + /** + * Safeguard + */ + @Rule + public Timeout timeout = new Timeout(10000); + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void load_properties_from_file() throws Exception { + File propsFile = temp.newFile(); + FileUtils.write(propsFile, "sonar.foo=bar"); + + ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(new String[]{propsFile.getAbsolutePath()}); + assertThat(entryPoint.getProps().value("sonar.foo")).isEqualTo("bar"); + } + + @Test + public void test_initial_state() throws Exception { + Props props = new Props(new Properties()); + ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + + assertThat(entryPoint.getProps()).isSameAs(props); + assertThat(entryPoint.isReady()).isFalse(); + assertThat(entryPoint.getState()).isEqualTo(State.INIT); + + // do not fail + entryPoint.ping(); + } + + @Test + public void fail_to_launch_if_missing_monitor_properties() throws Exception { + Props props = new Props(new Properties()); + ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + + StandardProcess process = new StandardProcess(); + try { + entryPoint.launch(process); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("Missing property: process.key"); + assertThat(process.getState()).isEqualTo(State.INIT); + } + } + + @Test + public void fail_to_launch_multiple_times() throws Exception { + Props props = new Props(new Properties()); + props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); + props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); + ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + + entryPoint.launch(new NoopProcess()); + try { + entryPoint.launch(new NoopProcess()); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Already started"); + } + } + + @Test + public void launch_then_request_graceful_termination() throws Exception { + Props props = new Props(new Properties()); + props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); + props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); + final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + final StandardProcess process = new StandardProcess(); + + Thread runner = new Thread() { + @Override + public void run() { + // starts and waits until terminated + entryPoint.launch(process); + } + }; + runner.start(); + + while (process.getState() != State.STARTED) { + Thread.sleep(10L); + } + + // requests for termination -> waits until down + // Should terminate before the timeout of 30s + entryPoint.terminate(); + + assertThat(process.getState()).isEqualTo(State.STOPPED); + } + + @Test + public void autokill_if_no_pings() throws Exception { + Props props = new Props(new Properties()); + props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test"); + props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_INTERVAL, "5"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_TIMEOUT, "1"); + final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + final StandardProcess process = new StandardProcess(); + + entryPoint.launch(process); + + assertThat(process.getState()).isEqualTo(State.STOPPED); + } + + @Test + public void terminate_if_unexpected_shutdown() throws Exception { + Props props = new Props(new Properties()); + props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "foo"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); + props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); + final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + final StandardProcess process = new StandardProcess(); + + Thread runner = new Thread() { + @Override + public void run() { + // starts and waits until terminated + entryPoint.launch(process); + } + }; + runner.start(); + while (process.getState() != State.STARTED) { + Thread.sleep(10L); + } + + // emulate signal to shutdown process + entryPoint.getShutdownHook().start(); + while (process.getState() != State.STOPPED) { + Thread.sleep(10L); + } + // exit before test timeout, ok ! + } + + @Test + public void terminate_if_startup_error() throws Exception { + Props props = new Props(new Properties()); + props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "foo"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); + props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); + final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + final MonitoredProcess process = new StartupErrorProcess(); + + entryPoint.launch(process); + assertThat(entryPoint.getState()).isEqualTo(State.STOPPED); + } + + private static class NoopProcess implements MonitoredProcess { + + @Override + public void start() { + + } + + @Override + public void awaitTermination() { + + } + + @Override + public void terminate() { + + } + } + + private static class StartupErrorProcess implements MonitoredProcess { + + @Override + public void start() { + throw new IllegalStateException("ERROR"); + } + + @Override + public void awaitTermination() { + + } + + @Override + public void terminate() { + + } + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java new file mode 100644 index 00000000000..6f93126516e --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java @@ -0,0 +1,28 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.junit.Test; + +import static org.fest.assertions.Assertions.assertThat; + +public class ProcessUtilsTest { + +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/PropsTest.java b/server/sonar-process/src/test/java/org/sonar/process/PropsTest.java new file mode 100644 index 00000000000..5d283b44f8f --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/PropsTest.java @@ -0,0 +1,135 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.junit.Test; + +import java.util.Properties; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + +public class PropsTest { + + @Test + public void of() throws Exception { + Properties p = new Properties(); + p.setProperty("foo", "bar"); + Props props = new Props(p); + + assertThat(props.value("foo")).isEqualTo("bar"); + assertThat(props.value("foo", "default value")).isEqualTo("bar"); + assertThat(props.value("unknown")).isNull(); + assertThat(props.value("unknown", "default value")).isEqualTo("default value"); + } + + @Test + public void intOf() throws Exception { + Properties p = new Properties(); + p.setProperty("foo", "33"); + p.setProperty("blank", ""); + Props props = new Props(p); + + assertThat(props.valueAsInt("foo")).isEqualTo(33); + assertThat(props.valueAsInt("foo", 44)).isEqualTo(33); + assertThat(props.valueAsInt("blank")).isNull(); + assertThat(props.valueAsInt("blank", 55)).isEqualTo(55); + assertThat(props.valueAsInt("unknown")).isNull(); + assertThat(props.valueAsInt("unknown", 44)).isEqualTo(44); + } + + @Test + public void intOf_not_integer() throws Exception { + Properties p = new Properties(); + p.setProperty("foo", "bar"); + Props props = new Props(p); + + try { + props.valueAsInt("foo"); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Value of property foo is not an integer: bar"); + } + } + + @Test + public void booleanOf() throws Exception { + Properties p = new Properties(); + p.setProperty("foo", "True"); + p.setProperty("bar", "false"); + Props props = new Props(p); + + assertThat(props.valueAsBoolean("foo")).isTrue(); + assertThat(props.valueAsBoolean("bar")).isFalse(); + assertThat(props.valueAsBoolean("unknown")).isFalse(); + } + + @Test + public void booleanOf_default_value() throws Exception { + Properties p = new Properties(); + p.setProperty("foo", "true"); + p.setProperty("bar", "false"); + Props props = new Props(p); + + assertThat(props.valueAsBoolean("unset", false)).isFalse(); + assertThat(props.valueAsBoolean("unset", true)).isTrue(); + assertThat(props.valueAsBoolean("foo", false)).isTrue(); + assertThat(props.valueAsBoolean("bar", true)).isFalse(); + } + + @Test + public void setDefault() throws Exception { + Properties p = new Properties(); + p.setProperty("foo", "foo_value"); + Props props = new Props(p); + props.setDefault("foo", "foo_def"); + props.setDefault("bar", "bar_def"); + + assertThat(props.value("foo")).isEqualTo("foo_value"); + assertThat(props.value("bar")).isEqualTo("bar_def"); + assertThat(props.value("other")).isNull(); + } + + @Test + public void set() throws Exception { + Properties p = new Properties(); + p.setProperty("foo", "old_foo"); + Props props = new Props(p); + props.set("foo", "new_foo"); + props.set("bar", "new_bar"); + + assertThat(props.value("foo")).isEqualTo("new_foo"); + assertThat(props.value("bar")).isEqualTo("new_bar"); + } + + @Test + public void raw_properties() throws Exception { + Properties p = new Properties(); + p.setProperty("encrypted_prop", "{aes}abcde"); + p.setProperty("clear_prop", "foo"); + Props props = new Props(p); + + assertThat(props.rawProperties()).hasSize(2); + // do not decrypt + assertThat(props.rawProperties().get("encrypted_prop")).isEqualTo("{aes}abcde"); + assertThat(props.rawProperties().get("clear_prop")).isEqualTo("foo"); + + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java b/server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java new file mode 100644 index 00000000000..e02b07cf555 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java @@ -0,0 +1,56 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.fest.assertions.Assertions.assertThat; + +public class SystemExitTest { + + @Test + public void do_not_exit_if_in_shutdown_hook() throws Exception { + SystemExit systemExit = new SystemExit(); + + systemExit.setInShutdownHook(); + assertThat(systemExit.isInShutdownHook()).isTrue(); + + systemExit.exit(0); + // still there + } + + @Test + public void exit_if_not_in_shutdown_hook() throws Exception { + final AtomicInteger got = new AtomicInteger(); + SystemExit systemExit = new SystemExit() { + @Override + void doExit(int code) { + got.set(code); + } + }; + + assertThat(systemExit.isInShutdownHook()).isFalse(); + systemExit.exit(1); + + assertThat(got.get()).isEqualTo(1); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java b/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java new file mode 100644 index 00000000000..307abed6d0b --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java @@ -0,0 +1,116 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.test; + +import org.apache.commons.io.FileUtils; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.sonar.process.MonitoredProcess; +import org.sonar.process.ProcessEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.File; +import java.io.IOException; + +/** + * Http server used for testing (see MonitorTest). It accepts HTTP commands /ping and /kill to hardly exit. + * It also pushes status to temp files, so test can verify what was really done (when server went ready state and + * if it was gracefully terminated) + */ +public class HttpProcess implements MonitoredProcess { + + private final Server server; + // temp dir is specific to this process + private final File tempDir = new File(System.getProperty("java.io.tmpdir")); + + public HttpProcess(int httpPort) { + server = new Server(httpPort); + } + + @Override + public void start() { + writeTimeToFile("startingAt"); + ContextHandler context = new ContextHandler(); + context.setContextPath("/"); + context.setClassLoader(Thread.currentThread().getContextClassLoader()); + server.setHandler(context); + context.setHandler(new AbstractHandler() { + @Override + public void handle(String target, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException { + if ("/ping".equals(target)) { + request.setHandled(true); + httpServletResponse.getWriter().print("ping"); + } else if ("/kill".equals(target)) { + writeTimeToFile("killedAt"); + System.exit(0); + } + } + }); + try { + server.start(); + while (!server.isStarted()) { + Thread.sleep(100L); + } + writeTimeToFile("readyAt"); + + } catch (Exception e) { + throw new IllegalStateException("Fail to start Jetty", e); + } + } + + @Override + public void awaitTermination() { + try { + server.join(); + } catch (InterruptedException ignore) { + + } + } + + @Override + public void terminate() { + try { + if (!server.isStopped()) { + server.stop(); + writeTimeToFile("terminatedAt"); + } + } catch (Exception e) { + throw new IllegalStateException("Fail to stop Jetty", e); + } + } + + private void writeTimeToFile(String filename) { + try { + FileUtils.write(new File(tempDir, filename), String.valueOf(System.currentTimeMillis())); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public static void main(String[] args) { + ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); + entryPoint.launch(new HttpProcess(entryPoint.getProps().valueAsInt("httpPort"))); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java b/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java new file mode 100644 index 00000000000..121784f633b --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java @@ -0,0 +1,81 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.test; + +import org.sonar.process.MonitoredProcess; +import org.sonar.process.ProcessEntryPoint; +import org.sonar.process.State; + +public class StandardProcess implements MonitoredProcess { + + private State state = State.INIT; + + private final Thread daemon = new Thread() { + @Override + public void run() { + try { + while (true) { + Thread.sleep(100L); + } + } catch (InterruptedException e) { + // return + } + } + }; + + /** + * Blocks until started() + */ + @Override + public void start() { + state = State.STARTING; + daemon.start(); + state = State.STARTED; + } + + @Override + public void awaitTermination() { + try { + daemon.join(); + } catch (InterruptedException e) { + // interrupted by call to terminate() + } + } + + /** + * Blocks until stopped + */ + @Override + public void terminate() { + state = State.STOPPING; + daemon.interrupt(); + state = State.STOPPED; + } + + public State getState() { + return state; + } + + public static void main(String[] args) { + ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); + entryPoint.launch(new StandardProcess()); + System.exit(0); + } +} diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt new file mode 100644 index 00000000000..65b98c522da --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt @@ -0,0 +1 @@ +0PZz+G+f8mjr3sPn4+AhHg== \ No newline at end of file diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt new file mode 100644 index 00000000000..b33e179e5c8 --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt @@ -0,0 +1 @@ +badbadbad== \ No newline at end of file diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt new file mode 100644 index 00000000000..ab83e4adc03 --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt @@ -0,0 +1,3 @@ + + 0PZz+G+f8mjr3sPn4+AhHg== + diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt new file mode 100644 index 00000000000..23f5ecf5104 --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt @@ -0,0 +1 @@ +IBxEUxZ41c8XTxyaah1Qlg== \ No newline at end of file diff --git a/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml b/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml new file mode 100644 index 00000000000..298193e01fa --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml @@ -0,0 +1 @@ + diff --git a/server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties b/server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties new file mode 100644 index 00000000000..1577a214b3b --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties @@ -0,0 +1,212 @@ +# This file must contain only ISO 8859-1 characters +# see http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Properties.html#load(java.io.InputStream) +# +# To use an environment variable, use the following syntax : ${env:NAME_OF_ENV_VARIABLE} +# For example: +# sonar.jdbc.url= ${env:SONAR_JDBC_URL} +# +# +# See also the file conf/wrapper.conf for JVM advanced settings + + + +#-------------------------------------------------------------------------------------------------- +# DATABASE +# +# IMPORTANT: the embedded H2 database is used by default. It is recommended for tests only. +# Please use a production-ready database. Supported databases are MySQL, Oracle, PostgreSQL +# and Microsoft SQLServer. + +# Permissions to create tables, indices and triggers must be granted to JDBC user. +# The schema must be created first. +sonar.jdbc.username=sonar +sonar.jdbc.password=sonar + +#----- Embedded database H2 +# Note: it does not accept connections from remote hosts, so the +# SonarQube server and the maven plugin must be executed on the same host. + +# Comment the following line to deactivate the default embedded database. +sonar.jdbc.url=jdbc:h2:tcp://localhost:9092/sonar + +# directory containing H2 database files. By default it's the /data directory in the SonarQube installation. +#sonar.embeddedDatabase.dataDir= +# H2 embedded database server listening port, defaults to 9092 +#sonar.embeddedDatabase.port=9092 + + +#----- MySQL 5.x +# Comment the embedded database and uncomment the following line to use MySQL +#sonar.jdbc.url=jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true + + +#----- Oracle 10g/11g +# To connect to Oracle database: +# +# - It's recommended to use the latest version of the JDBC driver (ojdbc6.jar). +# Download it in http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html +# - Copy the driver to the directory extensions/jdbc-driver/oracle/ +# - If you need to set the schema, please refer to http://jira.codehaus.org/browse/SONAR-5000 +# - Comment the embedded database and uncomment the following line: +#sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE + + +#----- PostgreSQL 8.x/9.x +# Comment the embedded database and uncomment the following property to use PostgreSQL. +# If you don't use the schema named "public", please refer to http://jira.codehaus.org/browse/SONAR-5000 +#sonar.jdbc.url=jdbc:postgresql://localhost/sonar + + +#----- Microsoft SQLServer +# The Jtds open source driver is available in extensions/jdbc-driver/mssql. More details on http://jtds.sourceforge.net +#sonar.jdbc.url=jdbc:jtds:sqlserver://localhost/sonar;SelectMethod=Cursor + + +#----- Connection pool settings +sonar.jdbc.maxActive=20 +sonar.jdbc.maxIdle=5 +sonar.jdbc.minIdle=2 +sonar.jdbc.maxWait=5000 +sonar.jdbc.minEvictableIdleTimeMillis=600000 +sonar.jdbc.timeBetweenEvictionRunsMillis=30000 + + + +#-------------------------------------------------------------------------------------------------- +# WEB SERVER + +# Binding IP address. For servers with more than one IP address, this property specifies which +# address will be used for listening on the specified ports. +# By default, ports will be used on all IP addresses associated with the server. +#sonar.web.host=0.0.0.0 + +# Web context. When set, it must start with forward slash (for example /sonarqube). +# The default value is root context (empty value). +#sonar.web.context= + +# TCP port for incoming HTTP connections. Disabled when value is -1. +#sonar.web.port=9000 + +# TCP port for incoming HTTPS connections. Disabled when value is -1 (default). +#sonar.web.https.port=-1 + +# HTTPS - the alias used to for the server certificate in the keystore. +# If not specified the first key read in the keystore is used. +#sonar.web.https.keyAlias= + +# HTTPS - the password used to access the server certificate from the +# specified keystore file. The default value is "changeit". +#sonar.web.https.keyPass=changeit + +# HTTPS - the pathname of the keystore file where is stored the server certificate. +# By default, the pathname is the file ".keystore" in the user home. +# If keystoreType doesn't need a file use empty value. +#sonar.web.https.keystoreFile= + +# HTTPS - the password used to access the specified keystore file. The default +# value is the value of sonar.web.https.keyPass. +#sonar.web.https.keystorePass= + +# HTTPS - the type of keystore file to be used for the server certificate. +# The default value is JKS (Java KeyStore). +#sonar.web.https.keystoreType=JKS + +# HTTPS - the name of the keystore provider to be used for the server certificate. +# If not specified, the list of registered providers is traversed in preference order +# and the first provider that supports the keystore type is used (see sonar.web.https.keystoreType). +#sonar.web.https.keystoreProvider= + +# HTTPS - the pathname of the truststore file which contains trusted certificate authorities. +# By default, this would be the cacerts file in your JRE. +# If truststoreFile doesn't need a file use empty value. +#sonar.web.https.truststoreFile= + +# HTTPS - the password used to access the specified truststore file. +#sonar.web.https.truststorePass= + +# HTTPS - the type of truststore file to be used. +# The default value is JKS (Java KeyStore). +#sonar.web.https.truststoreType=JKS + +# HTTPS - the name of the truststore provider to be used for the server certificate. +# If not specified, the list of registered providers is traversed in preference order +# and the first provider that supports the truststore type is used (see sonar.web.https.truststoreType). +#sonar.web.https.truststoreProvider= + +# HTTPS - whether to enable client certificate authentication. +# The default is false (client certificates disabled). +# Other possible values are 'want' (certificates will be requested, but not required), +# and 'true' (certificates are required). +#sonar.web.https.clientAuth=false + +# The maximum number of connections that the server will accept and process at any given time. +# When this number has been reached, the server will not accept any more connections until +# the number of connections falls below this value. The operating system may still accept connections +# based on the sonar.web.connections.acceptCount property. The default value is 50 for each +# enabled connector. +#sonar.web.http.maxThreads=50 +#sonar.web.https.maxThreads=50 + +# The minimum number of threads always kept running. The default value is 5 for each +# enabled connector. +#sonar.web.http.minThreads=5 +#sonar.web.https.minThreads=5 + +# The maximum queue length for incoming connection requests when all possible request processing +# threads are in use. Any requests received when the queue is full will be refused. +# The default value is 25 for each enabled connector. +#sonar.web.http.acceptCount=25 +#sonar.web.https.acceptCount=25 + +# Access logs are generated in the file logs/access.log. This file is rolled over when it's 5Mb. +# An archive of 3 files is kept in the same directory. +# Access logs are enabled by default. +#sonar.web.accessLogs.enable=true + +# TCP port for incoming AJP connections. Disabled when value is -1. +# sonar.ajp.port=9009 + + + +#-------------------------------------------------------------------------------------------------- +# UPDATE CENTER + +# The Update Center requires an internet connection to request http://update.sonarsource.org +# It is enabled by default. +#sonar.updatecenter.activate=true + +# HTTP proxy (default none) +#http.proxyHost= +#http.proxyPort= + +# NT domain name if NTLM proxy is used +#http.auth.ntlm.domain= + +# SOCKS proxy (default none) +#socksProxyHost= +#socksProxyPort= + +# proxy authentication. The 2 following properties are used for HTTP and SOCKS proxies. +#http.proxyUser= +#http.proxyPassword= + + +#-------------------------------------------------------------------------------------------------- +# NOTIFICATIONS + +# Delay in seconds between processing of notification queue. Default is 60. +#sonar.notifications.delay=60 + + +#-------------------------------------------------------------------------------------------------- +# PROFILING +# Level of information displayed in the logs: NONE (default), BASIC (functional information) and FULL (functional and technical details) +#sonar.log.profilingLevel=NONE + + +#-------------------------------------------------------------------------------------------------- +# DEVELOPMENT MODE +# Only for debugging + +# Set to true to apply Ruby on Rails code changes on the fly +#sonar.rails.dev=false diff --git a/server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties b/server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties new file mode 100644 index 00000000000..5c06e58a32e --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties @@ -0,0 +1,3 @@ +hello: world +foo=bar +java.io.tmpdir=/should/be/overridden diff --git a/server/sonar-process/src/test/resources/sonar-dummy-app.jar b/server/sonar-process/src/test/resources/sonar-dummy-app.jar new file mode 100644 index 00000000000..6dfd458329a Binary files /dev/null and b/server/sonar-process/src/test/resources/sonar-dummy-app.jar differ diff --git a/server/sonar-process/test-jar-with-dependencies.xml b/server/sonar-process/test-jar-with-dependencies.xml new file mode 100644 index 00000000000..832c66cc0be --- /dev/null +++ b/server/sonar-process/test-jar-with-dependencies.xml @@ -0,0 +1,19 @@ + + test-jar-with-dependencies + + jar + + false + + + / + true + + true + true + test + + + diff --git a/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java index 1abf733b66b..6e5a366ff64 100644 --- a/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java +++ b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java @@ -21,15 +21,14 @@ package org.sonar.search; import org.apache.commons.lang.StringUtils; import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus; -import org.elasticsearch.common.annotations.VisibleForTesting; import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.node.Node; import org.elasticsearch.node.NodeBuilder; import org.slf4j.LoggerFactory; -import org.sonar.process.ConfigurationUtils; import org.sonar.process.MinimumViableSystem; import org.sonar.process.MonitoredProcess; +import org.sonar.process.ProcessEntryPoint; import org.sonar.process.ProcessLogging; import org.sonar.process.Props; import org.sonar.search.script.ListUpdate; @@ -40,7 +39,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; -public class SearchServer extends MonitoredProcess { +public class SearchServer implements MonitoredProcess { public static final String SONAR_NODE_NAME = "sonar.node.name"; public static final String ES_PORT_PROPERTY = "sonar.search.port"; @@ -55,27 +54,13 @@ public class SearchServer extends MonitoredProcess { private static final Integer MINIMUM_INDEX_REPLICATION = 1; private final Set nodes = new HashSet(); - private final boolean isBlocking; - - private Node node; + private final Props props; private final Object lock = new Object(); - @VisibleForTesting - public SearchServer(final Props props, boolean monitored, boolean blocking) { - super(props, monitored); - - this.isBlocking = blocking; - new MinimumViableSystem().check(); - - String esNodesInets = props.value(ES_CLUSTER_INET); - if (StringUtils.isNotEmpty(esNodesInets)) { - Collections.addAll(nodes, esNodesInets.split(",")); - } - } + private Node node; public SearchServer(Props props) { - super(props); - this.isBlocking = true; + this.props = props; new MinimumViableSystem().check(); String esNodesInets = props.value(ES_CLUSTER_INET); @@ -85,18 +70,8 @@ public class SearchServer extends MonitoredProcess { } @Override - protected boolean doIsReady() { - return node.client().admin().cluster().prepareHealth() - .setWaitForYellowStatus() - .setTimeout(TimeValue.timeValueSeconds(3L)) - .get() - .getStatus() != ClusterHealthStatus.RED; - } - - @Override - protected void doStart() { + public void start() { synchronized (lock) { - Integer port = props.valueAsInt(ES_PORT_PROPERTY); String clusterName = props.value(ES_CLUSTER_PROPERTY); @@ -169,17 +144,25 @@ public class SearchServer extends MonitoredProcess { .addMapping("_default_", "{\"dynamic\": \"strict\"}") .get(); } + } - if (isBlocking) { - while (node != null && !node.isClosed()) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - // Ignore - } + boolean isReady() { + return node.client().admin().cluster().prepareHealth() + .setWaitForYellowStatus() + .setTimeout(TimeValue.timeValueSeconds(3L)) + .get() + .getStatus() != ClusterHealthStatus.RED; + } + + @Override + public void awaitTermination() { + while (node != null && !node.isClosed()) { + try { + Thread.sleep(200L); + } catch (InterruptedException e) { + // Ignore } } - } private void initAnalysis(ImmutableSettings.Builder esSettings) { @@ -188,40 +171,40 @@ public class SearchServer extends MonitoredProcess { // Disallow dynamic mapping (too expensive) .put("index.mapper.dynamic", false) - // Sortable text analyzer + // Sortable text analyzer .put("index.analysis.analyzer.sortable.type", "custom") .put("index.analysis.analyzer.sortable.tokenizer", "keyword") .putArray("index.analysis.analyzer.sortable.filter", "trim", "lowercase", "truncate") - // Edge NGram index-analyzer + // Edge NGram index-analyzer .put("index.analysis.analyzer.index_grams.type", "custom") .put("index.analysis.analyzer.index_grams.tokenizer", "whitespace") .putArray("index.analysis.analyzer.index_grams.filter", "trim", "lowercase", "gram_filter") - // Edge NGram search-analyzer + // Edge NGram search-analyzer .put("index.analysis.analyzer.search_grams.type", "custom") .put("index.analysis.analyzer.search_grams.tokenizer", "whitespace") .putArray("index.analysis.analyzer.search_grams.filter", "trim", "lowercase") - // Word index-analyzer + // Word index-analyzer .put("index.analysis.analyzer.index_words.type", "custom") .put("index.analysis.analyzer.index_words.tokenizer", "standard") .putArray("index.analysis.analyzer.index_words.filter", "standard", "word_filter", "lowercase", "stop", "asciifolding", "porter_stem") - // Word search-analyzer + // Word search-analyzer .put("index.analysis.analyzer.search_words.type", "custom") .put("index.analysis.analyzer.search_words.tokenizer", "standard") .putArray("index.analysis.analyzer.search_words.filter", "standard", "lowercase", "stop", "asciifolding", "porter_stem") - // Edge NGram filter + // Edge NGram filter .put("index.analysis.filter.gram_filter.type", "edgeNGram") .put("index.analysis.filter.gram_filter.min_gram", 2) .put("index.analysis.filter.gram_filter.max_gram", 15) .putArray("index.analysis.filter.gram_filter.token_chars", "letter", "digit", "punctuation", "symbol") - // Word filter + // Word filter .put("index.analysis.filter.word_filter.type", "word_delimiter") .put("index.analysis.filter.word_filter.generate_word_parts", true) .put("index.analysis.filter.word_filter.catenate_words", true) @@ -232,7 +215,7 @@ public class SearchServer extends MonitoredProcess { .put("index.analysis.filter.word_filter.split_on_numerics", true) .put("index.analysis.filter.word_filter.stem_english_possessive", true) - // Path Analyzer + // Path Analyzer .put("index.analysis.analyzer.path_analyzer.type", "custom") .put("index.analysis.analyzer.path_analyzer.tokenizer", "path_hierarchy"); @@ -267,18 +250,18 @@ public class SearchServer extends MonitoredProcess { } @Override - protected void doTerminate() { + public void terminate() { synchronized (lock) { - if (node != null && !node.isClosed()) { + if (!node.isClosed()) { node.close(); - node = null; } } } public static void main(String... args) { - Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args); - new ProcessLogging().configure(props, "/org/sonar/search/logback.xml"); - new SearchServer(props).start(); + ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); + new ProcessLogging().configure(entryPoint.getProps(), "/org/sonar/search/logback.xml"); + SearchServer searchServer = new SearchServer(entryPoint.getProps()); + entryPoint.launch(searchServer); } } diff --git a/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java b/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java index bdf54bb5187..54fb02435e9 100644 --- a/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java +++ b/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java @@ -26,16 +26,17 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.sonar.process.JmxUtils; -import org.sonar.process.MonitoredProcess; import org.sonar.process.Props; import javax.management.InstanceNotFoundException; import javax.management.MBeanRegistrationException; import javax.management.MBeanServer; + import java.io.IOException; import java.lang.management.ManagementFactory; import java.net.ServerSocket; @@ -44,6 +45,7 @@ import java.util.Properties; import static org.fest.assertions.Assertions.assertThat; import static org.junit.Assert.fail; +@Ignore public class SearchServerTest { @Rule @@ -81,7 +83,6 @@ public class SearchServerTest { @Test public void server_fail_to_start() throws Exception { Properties properties = new Properties(); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES"); searchServer = new SearchServer(new Props(properties)); new Thread(new Runnable() { @@ -107,7 +108,6 @@ public class SearchServerTest { @Test public void can_connect() throws Exception { Properties properties = new Properties(); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES"); properties.setProperty(SearchServer.SONAR_PATH_DATA, temp.newFolder().getAbsolutePath()); properties.setProperty(SearchServer.SONAR_PATH_TEMP, temp.newFolder().getAbsolutePath()); properties.setProperty(SearchServer.SONAR_PATH_LOG, temp.newFolder().getAbsolutePath()); diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java b/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java index 2356ff340e8..a7146eda913 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java @@ -19,22 +19,32 @@ */ package org.sonar.server.app; +import com.google.common.base.Throwables; +import com.google.common.util.concurrent.Uninterruptibles; +import org.apache.catalina.Container; +import org.apache.catalina.Executor; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Server; +import org.apache.catalina.Service; +import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardContext; import org.apache.catalina.startup.Tomcat; import org.apache.commons.io.FileUtils; import org.slf4j.LoggerFactory; -import org.sonar.process.ProcessUtils; import org.sonar.process.Props; -import org.sonar.process.Terminable; import java.io.File; +import java.util.concurrent.TimeUnit; -class EmbeddedTomcat implements Terminable { +class EmbeddedTomcat { private final Props props; private Tomcat tomcat = null; private Thread hook = null; - private boolean ready = false; + private volatile StandardContext webappContext; EmbeddedTomcat(Props props) { this.props = props; @@ -62,45 +72,52 @@ class EmbeddedTomcat implements Terminable { tomcat.getHost().setDeployOnStartup(true); Logging.configure(tomcat, props); Connectors.configure(tomcat, props); - StandardContext webappContext = Webapp.configure(tomcat, props); - ProcessUtils.addSelfShutdownHook(this); + webappContext = Webapp.configure(tomcat, props); tomcat.start(); + waitForWebappReady(); - if (webappContext.getState().isAvailable()) { - ready = true; - tomcat.getServer().await(); - } } catch (Exception e) { - throw new IllegalStateException("Fail to start web server", e); - } finally { - // Failed to start or received a shutdown command (should never occur as shutdown port is disabled) - terminate(); + Throwables.propagate(e); } } - private File tomcatBasedir() { - return new File(props.value("sonar.path.temp"), "tc"); + private void waitForWebappReady() { + while (true) { + switch (webappContext.getState()) { + case NEW: + case INITIALIZING: + case INITIALIZED: + case STARTING_PREP: + case STARTING: + Uninterruptibles.sleepUninterruptibly(300L, TimeUnit.MILLISECONDS); + break; + case STARTED: + // ok + return; + default: + // problem, stopped or failed + throw new IllegalStateException("YYY Webapp did not start"); + } + } } - boolean isReady() { - return ready && tomcat != null; + private File tomcatBasedir() { + return new File(props.value("sonar.path.temp"), "tc"); } - @Override - public void terminate() { - if (tomcat != null) { - synchronized (tomcat) { - if (tomcat.getServer().getState().isAvailable()) { - try { - tomcat.stop(); - tomcat.destroy(); - } catch (Exception e) { - LoggerFactory.getLogger(EmbeddedTomcat.class).error("Fail to stop web service", e); - } - } + void terminate() { + if (tomcat.getServer().getState().isAvailable()) { + try { + tomcat.stop(); + tomcat.destroy(); + } catch (Exception e) { + LoggerFactory.getLogger(EmbeddedTomcat.class).error("Fail to stop web server", e); } } - ready = false; FileUtils.deleteQuietly(tomcatBasedir()); } + + void awaitTermination() { + tomcat.getServer().await(); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java b/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java index d5bc4fde7de..94bc1214f0f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java @@ -19,18 +19,16 @@ */ package org.sonar.server.app; -import org.slf4j.LoggerFactory; -import org.sonar.process.ConfigurationUtils; import org.sonar.process.MinimumViableSystem; import org.sonar.process.MonitoredProcess; +import org.sonar.process.ProcessEntryPoint; import org.sonar.process.Props; -public class WebServer extends MonitoredProcess { +public class WebServer implements MonitoredProcess { private final EmbeddedTomcat tomcat; WebServer(Props props) throws Exception { - super(props); new MinimumViableSystem() .setRequiredJavaOption("file.encoding", "UTF-8") .check(); @@ -38,32 +36,27 @@ public class WebServer extends MonitoredProcess { } @Override - protected void doStart() { - try { - tomcat.start(); - } catch (Exception e) { - LoggerFactory.getLogger(getClass()).error("TC error", e); - } finally { - terminate(); - } + public void start() { + tomcat.start(); } @Override - protected void doTerminate() { + public void terminate() { tomcat.terminate(); } @Override - protected boolean doIsReady() { - return tomcat.isReady(); + public void awaitTermination() { + tomcat.awaitTermination(); } /** * Can't be started as is. Needs to be bootstrapped by sonar-application */ public static void main(String[] args) throws Exception { - Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args); - Logging.init(props); - new WebServer(props).start(); + ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); + Logging.init(entryPoint.getProps()); + WebServer server = new WebServer(entryPoint.getProps()); + entryPoint.launch(server); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java b/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java index f959faed106..8445efb22be 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java @@ -64,7 +64,6 @@ class Webapp { String key = entry.getKey().toString(); context.addParameter(key, entry.getValue().toString()); } - return context; } catch (Exception e) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java b/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java index 46bc0426333..33efbcd7e06 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java @@ -19,7 +19,7 @@ */ package org.sonar.server.platform; -import org.slf4j.LoggerFactory; +import com.google.common.base.Throwables; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; @@ -46,9 +46,9 @@ public final class PlatformServletContextListener implements ServletContextListe // - server does not stop if webapp fails at startup // - the second listener for jruby on rails is started even if this listener fails. It generates // unexpected errors - LoggerFactory.getLogger(getClass()).error("Fail to start server", t); + // LoggerFactory.getLogger(getClass()).error("Fail to start server", t); stopQuietly(); - throw new IllegalStateException("Fail to start webapp", t); + throw Throwables.propagate(t); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java b/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java index 3884439e166..5abc8a0eaef 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java +++ b/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java @@ -211,9 +211,11 @@ public class ServerPluginJarsInstaller { private void deploy(DefaultPluginMetadata plugin) { LOG.info("Deploy plugin {}", Joiner.on(" / ").skipNulls().join(plugin.getName(), plugin.getVersion(), plugin.getImplementationBuild())); - Preconditions.checkState(plugin.isCompatibleWith(server.getVersion()), - "Plugin %s needs a more recent version of SonarQube than %s. At least %s is expected", - plugin.getKey(), server.getVersion(), plugin.getSonarVersion()); + if (!plugin.isCompatibleWith(server.getVersion())) { + throw MessageException.of(String.format( + "Plugin %s needs a more recent version of SonarQube than %s. At least %s is expected", + plugin.getKey(), server.getVersion(), plugin.getSonarVersion())); + } try { File pluginDeployDir = new File(fs.getDeployedPluginsDir(), plugin.getKey()); diff --git a/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java b/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java index be40c087f88..90cce6fb4ed 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java @@ -85,7 +85,7 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); assertThat(new File(pluginsDir, "foo-plugin-1.0.jar")).exists().isFile(); PluginMetadata plugin = jarsInstaller.getMetadata("foo"); assertThat(plugin.getName()).isEqualTo("Foo"); @@ -101,7 +101,7 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).isEmpty(); } @Test @@ -115,7 +115,7 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); // do not copy foo 1.0 - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(2); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(2); assertThat(new File(pluginsDir, "foo-plugin-2.0.jar")).exists().isFile(); assertThat(new File(pluginsDir, "bar-plugin-1.0.jar")).exists().isFile(); PluginMetadata plugin = jarsInstaller.getMetadata("foo"); @@ -138,7 +138,7 @@ public class ServerPluginJarsInstallerTest { assertThat(plugin.isUseChildFirstClassLoader()).isFalse(); // check that the file is still present in extensions/plugins - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); assertThat(new File(pluginsDir, "foo-plugin-1.0.jar")).exists().isFile(); } @@ -151,13 +151,13 @@ public class ServerPluginJarsInstallerTest { // nothing to install but keep the file assertThat(jarsInstaller.getMetadata()).isEmpty(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); assertThat(new File(pluginsDir, "not-a-plugin.jar")).exists().isFile(); } @Test public void fail_if_plugin_requires_greater_SQ_version() throws Exception { - exception.expect(IllegalStateException.class); + exception.expect(MessageException.class); exception.expectMessage("Plugin switchoffviolations needs a more recent version of SonarQube than 2.0. At least 2.5 is expected"); when(upgradeStatus.isFreshInstall()).thenReturn(false); @@ -174,8 +174,8 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); - assertThat(FileUtils.listFiles(downloadsDir, new String[]{"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(downloadsDir, new String[] {"jar"}, false)).isEmpty(); assertThat(new File(pluginsDir, "foo-plugin-1.0.jar")).exists().isFile(); } @@ -187,8 +187,8 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); - assertThat(FileUtils.listFiles(downloadsDir, new String[]{"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(downloadsDir, new String[] {"jar"}, false)).isEmpty(); assertThat(new File(pluginsDir, "foo-plugin-2.0.jar")).exists().isFile(); } @@ -205,8 +205,8 @@ public class ServerPluginJarsInstallerTest { PluginMetadata plugin = jarsInstaller.getMetadata("foo"); assertThat(plugin).isNotNull(); assertThat(plugin.getVersion()).isEqualTo("2.0"); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); - assertThat(FileUtils.listFiles(downloadsDir, new String[]{"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(downloadsDir, new String[] {"jar"}, false)).isEmpty(); File installed = new File(pluginsDir, "foo-plugin-1.0.jar"); assertThat(installed).exists().isFile(); } @@ -218,7 +218,7 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).isEmpty(); assertThat(trashDir).doesNotExist(); } @@ -247,8 +247,8 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); jarsInstaller.uninstall("foo"); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).isEmpty(); - assertThat(FileUtils.listFiles(trashDir, new String[]{"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(trashDir, new String[] {"jar"}, false)).hasSize(1); assertThat(jarsInstaller.getUninstalls()).containsOnly("foo-plugin-1.0.jar"); } @@ -261,8 +261,8 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.uninstall("foo"); jarsInstaller.cancelUninstalls(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); - assertThat(FileUtils.listFiles(trashDir, new String[]{"jar"}, false)).hasSize(0); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(trashDir, new String[] {"jar"}, false)).hasSize(0); assertThat(jarsInstaller.getUninstalls()).isEmpty(); } @@ -274,10 +274,10 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); // do not deploy in extensions/plugins - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(0); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(0); // do not remove from lib/core-plugins - assertThat(FileUtils.listFiles(coreDir, new String[]{"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(coreDir, new String[] {"jar"}, false)).hasSize(1); PluginMetadata plugin = jarsInstaller.getMetadata("foo"); assertThat(plugin).isNotNull(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java index e92fd5b936f..8270a46f06e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java @@ -61,10 +61,9 @@ public class BaseIndexTest { Properties properties = new Properties(); properties.setProperty(IndexProperties.CLUSTER_NAME, clusterName); properties.setProperty(IndexProperties.NODE_PORT, clusterPort.toString()); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES"); properties.setProperty(SearchServer.SONAR_PATH_HOME, temp.getRoot().getAbsolutePath()); try { - searchServer = new SearchServer(new Props(properties), false, false); + searchServer = new SearchServer(new Props(properties)); } catch (Exception e) { e.printStackTrace(); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java b/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java index ce6fad22f77..1a935cb2394 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java +++ b/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java @@ -26,7 +26,6 @@ import org.apache.commons.lang.StringUtils; import org.junit.rules.ExternalResource; import org.sonar.api.database.DatabaseProperties; import org.sonar.api.resources.Language; -import org.sonar.process.MonitoredProcess; import org.sonar.process.NetworkUtils; import org.sonar.process.Props; import org.sonar.search.SearchServer; @@ -35,6 +34,7 @@ import org.sonar.server.search.IndexProperties; import org.sonar.server.ws.WsTester; import javax.annotation.Nullable; + import java.io.File; import java.util.Arrays; import java.util.List; @@ -71,13 +71,8 @@ public class ServerTester extends ExternalResource { Properties properties = new Properties(); properties.setProperty(IndexProperties.CLUSTER_NAME, clusterName); properties.setProperty(IndexProperties.NODE_PORT, clusterPort.toString()); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES"); properties.setProperty(SearchServer.SONAR_PATH_HOME, homeDir.getAbsolutePath()); - try { - searchServer = new SearchServer(new Props(properties), false, false); - } catch (Exception e) { - e.printStackTrace(); - } + searchServer = new SearchServer(new Props(properties)); } /** @@ -99,7 +94,6 @@ public class ServerTester extends ExternalResource { properties.setProperty(IndexProperties.CLUSTER_NAME, clusterName); properties.setProperty(IndexProperties.NODE_PORT, clusterPort.toString()); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES"); properties.setProperty("sonar.path.home", homeDir.getAbsolutePath()); properties.setProperty(DatabaseProperties.PROP_URL, "jdbc:h2:" + homeDir.getAbsolutePath() + "/h2"); diff --git a/sonar-application/pom.xml b/sonar-application/pom.xml index a7e45403831..bfa0f4406df 100644 --- a/sonar-application/pom.xml +++ b/sonar-application/pom.xml @@ -25,6 +25,11 @@ sonar-process ${project.version} + + org.codehaus.sonar + sonar-process-monitor + ${project.version} + com.google.code.findbugs diff --git a/sonar-application/src/main/java/org/sonar/application/App.java b/sonar-application/src/main/java/org/sonar/application/App.java index 76ac6a16605..42c4bba84ce 100644 --- a/sonar-application/src/main/java/org/sonar/application/App.java +++ b/sonar-application/src/main/java/org/sonar/application/App.java @@ -21,19 +21,18 @@ package org.sonar.application; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.sonar.process.JmxUtils; import org.sonar.process.MinimumViableSystem; -import org.sonar.process.Monitor; import org.sonar.process.ProcessLogging; import org.sonar.process.ProcessMXBean; -import org.sonar.process.ProcessUtils; -import org.sonar.process.ProcessWrapper; import org.sonar.process.Props; -import org.sonar.search.SearchServer; +import org.sonar.process.State; +import org.sonar.process.monitor.JavaCommand; +import org.sonar.process.monitor.Monitor; import java.io.File; +import java.util.ArrayList; +import java.util.List; import java.util.Properties; /** @@ -41,135 +40,88 @@ import java.util.Properties; */ public class App implements ProcessMXBean { - private Monitor monitor = new Monitor(); - private ProcessWrapper elasticsearch; - private ProcessWrapper server; - private boolean success = false; + private final Monitor monitor; public App() { - JmxUtils.registerMBean(this, "SonarQube"); - ProcessUtils.addSelfShutdownHook(this); + this(Monitor.create()); } - public void start(Props props) throws InterruptedException { - try { - Logger logger = LoggerFactory.getLogger(getClass()); + App(Monitor monitor) { + this.monitor = monitor; + JmxUtils.registerMBean(this, "SonarQube"); + } - monitor.start(); + public void start(Props props) { + monitor.start(createCommands(props)); + monitor.awaitTermination(); + } - File homeDir = props.nonNullValueAsFile("sonar.path.home"); - File tempDir = props.nonNullValueAsFile("sonar.path.temp"); - elasticsearch = new ProcessWrapper(JmxUtils.SEARCH_SERVER_NAME); - elasticsearch + private List createCommands(Props props) { + List commands = new ArrayList(); + File homeDir = props.nonNullValueAsFile("sonar.path.home"); + File tempDir = props.nonNullValueAsFile("sonar.path.temp"); + JavaCommand elasticsearch = new JavaCommand(JmxUtils.SEARCH_SERVER_NAME); + elasticsearch + .setWorkDir(homeDir) + .setJmxPort(props.valueAsInt(DefaultSettings.SEARCH_JMX_PORT)) + .addJavaOptions(props.value(DefaultSettings.SEARCH_JAVA_OPTS)) + .setTempDir(tempDir.getAbsoluteFile()) + .setClassName("org.sonar.search.SearchServer") + .setArguments(props.rawProperties()) + .addClasspath("./lib/common/*") + .addClasspath("./lib/search/*"); + commands.add(elasticsearch); + + // do not yet start SQ in cluster mode. See SONAR-5483 & SONAR-5391 + if (StringUtils.isEmpty(props.value(DefaultSettings.CLUSTER_MASTER))) { + JavaCommand webServer = new JavaCommand(JmxUtils.WEB_SERVER_NAME) .setWorkDir(homeDir) - .setJmxPort(props.valueAsInt(DefaultSettings.SEARCH_JMX_PORT)) - .addJavaOpts(props.value(DefaultSettings.SEARCH_JAVA_OPTS)) - .setTempDirectory(tempDir.getAbsoluteFile()) - .setClassName("org.sonar.search.SearchServer") - .addProperties(props.rawProperties()) + .setJmxPort(props.valueAsInt(DefaultSettings.WEB_JMX_PORT)) + .addJavaOptions(props.nonNullValue(DefaultSettings.WEB_JAVA_OPTS)) + .setTempDir(tempDir.getAbsoluteFile()) + // required for logback tomcat valve + .setEnvVariable("sonar.path.logs", props.nonNullValue("sonar.path.logs")) + .setClassName("org.sonar.server.app.WebServer") + .setArguments(props.rawProperties()) .addClasspath("./lib/common/*") - .addClasspath("./lib/search/*"); - if (elasticsearch.execute()) { - monitor.monitor(elasticsearch); - if (elasticsearch.waitForReady()) { - logger.info("search server is up"); - - // do not yet start SQ in cluster mode. See SONAR-5483 & SONAR-5391 - if (StringUtils.isEmpty(props.value(DefaultSettings.CLUSTER_MASTER))) { - server = new ProcessWrapper(JmxUtils.WEB_SERVER_NAME) - .setWorkDir(homeDir) - .setJmxPort(props.valueAsInt(DefaultSettings.WEB_JMX_PORT)) - .addJavaOpts(props.nonNullValue(DefaultSettings.WEB_JAVA_OPTS)) - .setTempDirectory(tempDir.getAbsoluteFile()) - // required for logback tomcat valve - .setLogDir(props.nonNullValueAsFile("sonar.path.logs")) - .setClassName("org.sonar.server.app.WebServer") - .addProperties(props.rawProperties()) - .addClasspath("./lib/common/*") - .addClasspath("./lib/server/*"); - String driverPath = props.value(JdbcSettings.PROPERTY_DRIVER_PATH); - if (driverPath != null) { - server.addClasspath(driverPath); - } - if (server.execute()) { - monitor.monitor(server); - if (server.waitForReady()) { - success = true; - logger.info("web server is up"); - } - } - } else { - success = true; - } - } + .addClasspath("./lib/server/*"); + String driverPath = props.value(JdbcSettings.PROPERTY_DRIVER_PATH); + if (driverPath != null) { + webServer.addClasspath(driverPath); } - } finally { - monitor.join(); - terminate(); + commands.add(webServer); } + return commands; } - static String starPath(File homeDir, String relativePath) { - File dir = new File(homeDir, relativePath); - return FilenameUtils.concat(dir.getAbsolutePath(), "*"); + @Override + public void terminate() { + monitor.stop(); } @Override public boolean isReady() { - return monitor.isAlive(); + return monitor.getState() == State.STARTED; } @Override - public long ping() { - return System.currentTimeMillis(); - } + public void ping() { - @Override - public void terminate() { - if (monitor != null && monitor.isAlive()) { - monitor.terminate(); - monitor = null; - } - if (server != null) { - server.terminate(); - server = null; - } - if (elasticsearch != null) { - elasticsearch.terminate(); - elasticsearch = null; - } } - private boolean isSuccess() { - return success; + static String starPath(File homeDir, String relativePath) { + File dir = new File(homeDir, relativePath); + return FilenameUtils.concat(dir.getAbsolutePath(), "*"); } - public static void main(String[] args) { + public static void main(String[] args) throws Exception { new MinimumViableSystem().check(); CommandLineParser cli = new CommandLineParser(); Properties rawProperties = cli.parseArguments(args); - Props props; - - try { - props = new PropsBuilder(rawProperties, new JdbcSettings()).build(); - new ProcessLogging().configure(props, "/org/sonar/application/logback.xml"); - } catch (Exception e) { - throw new IllegalStateException(e); - } + Props props = new PropsBuilder(rawProperties, new JdbcSettings()).build(); + new ProcessLogging().configure(props, "/org/sonar/application/logback.xml"); App app = new App(); - ProcessUtils.addSelfShutdownHook(app); - try { - // start and wait for shutdown command - if (props.contains(SearchServer.ES_CLUSTER_INET)) { - LoggerFactory.getLogger(App.class).info("SonarQube slave configured to join SonarQube master : {}", props.value(SearchServer.ES_CLUSTER_INET)); - } - app.start(props); - } catch (InterruptedException e) { - LoggerFactory.getLogger(App.class).info("interrupted"); - } finally { - LoggerFactory.getLogger(App.class).info("stopped"); - System.exit(app.isSuccess() ? 0 : 1); - } + app.start(props); } } -- cgit v1.2.3