@@ -44,23 +44,18 @@ public class JavaProcessLauncher { | |||
ProcessRef launch(JavaCommand command) { | |||
Process process = null; | |||
try { | |||
// cleanup existing monitor file. Child process creates it when ready. | |||
// TODO fail if impossible to delete | |||
// cleanup existing monitor files | |||
ProcessCommands commands = new ProcessCommands(command.getTempDir(), command.getKey()); | |||
commands.prepareMonitor(); | |||
commands.prepare(); | |||
ProcessBuilder processBuilder = create(command); | |||
LoggerFactory.getLogger(getClass()).info("Launch {}: {}", | |||
command.getKey(), StringUtils.join(processBuilder.command(), " ")); | |||
long startedAt = System.currentTimeMillis(); | |||
process = processBuilder.start(); | |||
StreamGobbler inputGobbler = new StreamGobbler(process.getInputStream(), command.getKey()); | |||
inputGobbler.start(); | |||
ProcessRef ref = new ProcessRef(command.getKey(), commands, process, inputGobbler); | |||
ref.setLaunchedAt(startedAt); | |||
return ref; | |||
return new ProcessRef(command.getKey(), commands, process, inputGobbler); | |||
} catch (Exception e) { | |||
// just in case |
@@ -137,16 +137,12 @@ public class Monitor { | |||
/** | |||
* Asks for processes termination and returns without blocking until termination. | |||
* @return true if termination was requested, false if it was already being terminated | |||
*/ | |||
boolean stopAsync() { | |||
boolean requested = false; | |||
public void stopAsync() { | |||
if (lifecycle.tryToMoveTo(State.STOPPING)) { | |||
requested = true; | |||
terminator.setProcesses(processes); | |||
terminator.start(); | |||
} | |||
return requested; | |||
} | |||
public State getState() { |
@@ -30,7 +30,6 @@ class ProcessRef { | |||
private final ProcessCommands commands; | |||
private final Process process; | |||
private final StreamGobbler gobbler; | |||
private long launchedAt; | |||
private volatile boolean stopped = false; | |||
ProcessRef(String key, ProcessCommands commands, Process process, StreamGobbler gobbler) { | |||
@@ -61,7 +60,7 @@ class ProcessRef { | |||
if (isStopped()) { | |||
throw new MessageException(String.format("%s failed to start", this)); | |||
} | |||
ready = commands.wasReadyAfter(launchedAt); | |||
ready = commands.isReady(); | |||
try { | |||
Thread.sleep(200L); | |||
} catch (InterruptedException e) { | |||
@@ -70,10 +69,6 @@ class ProcessRef { | |||
} | |||
} | |||
void setLaunchedAt(long launchedAt) { | |||
this.launchedAt = launchedAt; | |||
} | |||
/** | |||
* True if process is physically down | |||
*/ |
@@ -57,22 +57,19 @@ public class ProcessCommands { | |||
this.stopFile = stopFile; | |||
} | |||
/** | |||
* Executed by monitor - delete shared files before starting child process | |||
*/ | |||
public void prepareMonitor() { | |||
public void prepare() { | |||
deleteFile(readyFile); | |||
deleteFile(stopFile); | |||
} | |||
public void finalizeProcess() { | |||
public void endWatch() { | |||
// do not fail if files can't be deleted | |||
FileUtils.deleteQuietly(readyFile); | |||
FileUtils.deleteQuietly(stopFile); | |||
} | |||
public boolean wasReadyAfter(long launchedAt) { | |||
return isCreatedAfter(readyFile, launchedAt); | |||
public boolean isReady() { | |||
return readyFile.exists(); | |||
} | |||
/** | |||
@@ -89,8 +86,8 @@ public class ProcessCommands { | |||
createFile(stopFile); | |||
} | |||
public boolean askedForStopAfter(long launchedAt) { | |||
return isCreatedAfter(stopFile, launchedAt); | |||
public boolean askedForStop() { | |||
return stopFile.exists(); | |||
} | |||
File getReadyFile() { | |||
@@ -117,9 +114,4 @@ public class ProcessCommands { | |||
} | |||
} | |||
} | |||
private boolean isCreatedAfter(File file, long launchedAt) { | |||
// File#lastModified() can have second precision on some OS | |||
return file.exists() && file.lastModified() / 1000 >= launchedAt / 1000; | |||
} | |||
} |
@@ -21,7 +21,7 @@ package org.sonar.process; | |||
import org.slf4j.LoggerFactory; | |||
public class ProcessEntryPoint { | |||
public class ProcessEntryPoint implements Stoppable { | |||
public static final String PROPERTY_PROCESS_KEY = "process.key"; | |||
public static final String PROPERTY_TERMINATION_TIMEOUT = "process.terminationTimeout"; | |||
@@ -32,7 +32,6 @@ public class ProcessEntryPoint { | |||
private final ProcessCommands commands; | |||
private final SystemExit exit; | |||
private volatile Monitored monitored; | |||
private volatile long launchedAt; | |||
private volatile StopperThread stopperThread; | |||
private final StopWatcher stopWatcher; | |||
@@ -49,7 +48,6 @@ public class ProcessEntryPoint { | |||
this.props = props; | |||
this.exit = exit; | |||
this.commands = commands; | |||
this.launchedAt = System.currentTimeMillis(); | |||
this.stopWatcher = new StopWatcher(commands, this); | |||
} | |||
@@ -68,6 +66,7 @@ public class ProcessEntryPoint { | |||
if (!lifecycle.tryToMoveTo(Lifecycle.State.STARTING)) { | |||
throw new IllegalStateException("Already started"); | |||
} | |||
commands.prepare(); | |||
monitored = mp; | |||
try { | |||
@@ -116,10 +115,12 @@ public class ProcessEntryPoint { | |||
exit.exit(0); | |||
} | |||
void stopAsync() { | |||
@Override | |||
public void stopAsync() { | |||
if (lifecycle.tryToMoveTo(Lifecycle.State.STOPPING)) { | |||
stopperThread = new StopperThread(monitored, commands, Long.parseLong(props.nonNullValue(PROPERTY_TERMINATION_TIMEOUT))); | |||
stopperThread.start(); | |||
stopWatcher.stopWatching(); | |||
} | |||
} | |||
@@ -131,10 +132,6 @@ public class ProcessEntryPoint { | |||
return shutdownHook; | |||
} | |||
long getLaunchedAt() { | |||
return launchedAt; | |||
} | |||
public static ProcessEntryPoint createForArguments(String[] args) { | |||
Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args); | |||
ProcessCommands commands = new ProcessCommands( |
@@ -23,31 +23,35 @@ import org.slf4j.LoggerFactory; | |||
public class StopWatcher extends Thread { | |||
private final ProcessEntryPoint process; | |||
private final Stoppable stoppable; | |||
private final ProcessCommands commands; | |||
private boolean watching = true; | |||
public StopWatcher(ProcessCommands commands, ProcessEntryPoint process) { | |||
public StopWatcher(ProcessCommands commands, Stoppable stoppable) { | |||
super("Stop Watcher"); | |||
this.commands = commands; | |||
this.process = process; | |||
this.stoppable = stoppable; | |||
} | |||
@Override | |||
public void run() { | |||
while (watching) { | |||
if (commands.askedForStopAfter(process.getLaunchedAt())) { | |||
LoggerFactory.getLogger(getClass()).info("Stopping process"); | |||
process.stopAsync(); | |||
watching = false; | |||
} else { | |||
try { | |||
Thread.sleep(500L); | |||
} catch (InterruptedException ignored) { | |||
commands.prepare(); | |||
try { | |||
while (watching) { | |||
if (commands.askedForStop()) { | |||
LoggerFactory.getLogger(getClass()).info("Stopping process"); | |||
stoppable.stopAsync(); | |||
watching = false; | |||
} else { | |||
try { | |||
Thread.sleep(500L); | |||
} catch (InterruptedException ignored) { | |||
watching = false; | |||
} | |||
} | |||
} | |||
} finally { | |||
commands.endWatch(); | |||
} | |||
} | |||
@@ -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 interface Stoppable { | |||
void stopAsync(); | |||
} |
@@ -57,6 +57,6 @@ class StopperThread extends Thread { | |||
LoggerFactory.getLogger(getClass()).error(String.format("Can not stop in %dms", terminationTimeout), e); | |||
} | |||
executor.shutdownNow(); | |||
commands.finalizeProcess(); | |||
commands.endWatch(); | |||
} | |||
} |
@@ -40,11 +40,11 @@ public class ProcessCommandsTest { | |||
public void delete_files_on_monitor_startup() throws Exception { | |||
File dir = temp.newFolder(); | |||
assertThat(dir).exists(); | |||
FileUtils.touch(new File(dir, "WEB.ready")); | |||
FileUtils.touch(new File(dir, "WEB.stop")); | |||
FileUtils.touch(new File(dir, "web.ready")); | |||
FileUtils.touch(new File(dir, "web.stop")); | |||
ProcessCommands commands = new ProcessCommands(dir, "WEB"); | |||
commands.prepareMonitor(); | |||
ProcessCommands commands = new ProcessCommands(dir, "web"); | |||
commands.prepare(); | |||
assertThat(commands.getReadyFile()).doesNotExist(); | |||
assertThat(commands.getStopFile()).doesNotExist(); | |||
@@ -58,7 +58,7 @@ public class ProcessCommandsTest { | |||
ProcessCommands commands = new ProcessCommands(readyFile, temp.newFile()); | |||
try { | |||
commands.prepareMonitor(); | |||
commands.prepare(); | |||
fail(); | |||
} catch (MessageException e) { | |||
// ok | |||
@@ -70,39 +70,15 @@ public class ProcessCommandsTest { | |||
File readyFile = temp.newFile(); | |||
ProcessCommands commands = new ProcessCommands(readyFile, temp.newFile()); | |||
commands.prepareMonitor(); | |||
commands.prepare(); | |||
assertThat(commands.isReady()).isFalse(); | |||
assertThat(readyFile).doesNotExist(); | |||
commands.setReady(); | |||
assertThat(commands.isReady()).isTrue(); | |||
assertThat(readyFile).exists(); | |||
commands.finalizeProcess(); | |||
commands.endWatch(); | |||
assertThat(readyFile).doesNotExist(); | |||
} | |||
@Test | |||
public void was_ready_after_date() throws Exception { | |||
File readyFile = mock(File.class); | |||
ProcessCommands commands = new ProcessCommands(readyFile, temp.newFile()); | |||
// does not exist | |||
when(readyFile.exists()).thenReturn(false); | |||
when(readyFile.lastModified()).thenReturn(123456L); | |||
assertThat(commands.wasReadyAfter(122000L)).isFalse(); | |||
// readyFile created before | |||
when(readyFile.exists()).thenReturn(true); | |||
when(readyFile.lastModified()).thenReturn(123456L); | |||
assertThat(commands.wasReadyAfter(124000L)).isFalse(); | |||
// readyFile created after | |||
when(readyFile.exists()).thenReturn(true); | |||
when(readyFile.lastModified()).thenReturn(123456L); | |||
assertThat(commands.wasReadyAfter(123123L)).isTrue(); | |||
// readyFile created after, but can be truncated to second on some OS | |||
when(readyFile.exists()).thenReturn(true); | |||
when(readyFile.lastModified()).thenReturn(123000L); | |||
assertThat(commands.wasReadyAfter(123456L)).isTrue(); | |||
} | |||
} |
@@ -22,8 +22,11 @@ package org.sonar.application; | |||
import org.apache.commons.io.FilenameUtils; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.sonar.process.MinimumViableSystem; | |||
import org.sonar.process.ProcessCommands; | |||
import org.sonar.process.ProcessLogging; | |||
import org.sonar.process.Props; | |||
import org.sonar.process.StopWatcher; | |||
import org.sonar.process.Stoppable; | |||
import org.sonar.process.monitor.JavaCommand; | |||
import org.sonar.process.monitor.Monitor; | |||
@@ -35,7 +38,7 @@ import java.util.Properties; | |||
/** | |||
* Entry-point of process that starts and monitors elasticsearch and web servers | |||
*/ | |||
public class App { | |||
public class App implements Stoppable { | |||
private final Monitor monitor; | |||
@@ -48,6 +51,12 @@ public class App { | |||
} | |||
public void start(Props props) { | |||
if (props.valueAsBoolean("sonar.enableStopCommand", false)) { | |||
// stop application when file <temp>/app.stop is created | |||
File tempDir = props.nonNullValueAsFile("sonar.path.temp"); | |||
ProcessCommands commands = new ProcessCommands(tempDir, "app"); | |||
new StopWatcher(commands, this).start(); | |||
} | |||
monitor.start(createCommands(props)); | |||
monitor.awaitTermination(); | |||
} | |||
@@ -103,4 +112,9 @@ public class App { | |||
App app = new App(); | |||
app.start(props); | |||
} | |||
@Override | |||
public void stopAsync() { | |||
monitor.stopAsync(); | |||
} | |||
} |