/* * SonarQube * Copyright (C) 2009-2025 SonarSource SA * mailto:info AT sonarsource DOT com * * This program 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. * * This program 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.io.File; import java.util.function.Predicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.process.sharedmemoryfile.DefaultProcessCommands; import org.sonar.process.sharedmemoryfile.ProcessCommands; import static org.sonar.process.Lifecycle.State.STOPPED; public class ProcessEntryPoint { public static final String PROPERTY_PROCESS_KEY = "process.key"; public static final String PROPERTY_PROCESS_INDEX = "process.index"; public static final String PROPERTY_GRACEFUL_STOP_TIMEOUT_MS = "process.gracefulStopTimeout"; public static final String PROPERTY_SHARED_PATH = "process.sharedDir"; // 1 second private static final long HARD_STOP_TIMEOUT_MS = 1_000L; private final Props props; private final ProcessId processId; private final Lifecycle lifecycle = new Lifecycle(); private final ProcessCommands commands; private final SystemExit exit; private final StopWatcher stopWatcher; private final StopWatcher hardStopWatcher; // new Runnable() is important to avoid conflict of call to ProcessEntryPoint#stop() with Thread#stop() private final Runtime runtime; private Monitored monitored; private volatile StopperThread stopperThread; public ProcessEntryPoint(Props props, SystemExit exit, ProcessCommands commands, Runtime runtime) { this.props = props; this.processId = ProcessId.fromKey(props.nonNullValue(PROPERTY_PROCESS_KEY)); this.exit = exit; this.commands = commands; this.stopWatcher = createStopWatcher(commands, this); this.hardStopWatcher = createHardStopWatcher(commands, this); this.runtime = runtime; } public ProcessCommands getCommands() { return commands; } public Props getProps() { return props; } /** * Launch process and waits until it's down */ public void launch(Monitored mp) { if (!lifecycle.tryToMoveTo(Lifecycle.State.STARTING)) { throw new IllegalStateException("Already started"); } monitored = mp; Logger logger = LoggerFactory.getLogger(getClass()); try { launch(logger); } catch (Exception e) { logger.warn("Fail to start {}", processId.getHumanReadableName(), e); hardStop(); } } private void launch(Logger logger) throws InterruptedException { logger.info("Starting {}", processId.getHumanReadableName()); runtime.addShutdownHook(new Thread(() -> { exit.setInShutdownHook(); stop(); })); stopWatcher.start(); hardStopWatcher.start(); monitored.start(); Monitored.Status status = waitForStatus(s -> s != Monitored.Status.DOWN); if (status == Monitored.Status.UP || status == Monitored.Status.OPERATIONAL) { // notify monitor that process is ready commands.setUp(); if (lifecycle.tryToMoveTo(Lifecycle.State.STARTED)) { Monitored.Status newStatus = waitForStatus(s -> s == Monitored.Status.OPERATIONAL || s == Monitored.Status.FAILED); if (newStatus == Monitored.Status.OPERATIONAL && lifecycle.tryToMoveTo(Lifecycle.State.OPERATIONAL)) { commands.setOperational(); } monitored.awaitStop(); } } else { logger.trace("Fail to start. Hard stopping..."); hardStop(); } } private Monitored.Status waitForStatus(Predicate statusPredicate) throws InterruptedException { Monitored.Status status = monitored.getStatus(); while (!statusPredicate.test(status)) { Thread.sleep(20); status = monitored.getStatus(); } return status; } void stop() { stopAsync(); monitored.awaitStop(); } /** * Blocks until stopped in a timely fashion (see {@link HardStopperThread}) */ void hardStop() { hardStopAsync(); monitored.awaitStop(); } private void stopAsync() { if (lifecycle.tryToMoveTo(Lifecycle.State.STOPPING)) { LoggerFactory.getLogger(ProcessEntryPoint.class).info("Gracefully stopping process"); stopWatcher.stopWatching(); long terminationTimeoutMs = Long.parseLong(props.nonNullValue(PROPERTY_GRACEFUL_STOP_TIMEOUT_MS)); stopperThread = new StopperThread(monitored, this::terminate, terminationTimeoutMs); stopperThread.start(); } } private void hardStopAsync() { if (lifecycle.tryToMoveTo(Lifecycle.State.HARD_STOPPING)) { LoggerFactory.getLogger(ProcessEntryPoint.class).info("Hard stopping process"); if (stopperThread != null) { stopperThread.stopIt(); } hardStopWatcher.stopWatching(); stopWatcher.stopWatching(); new HardStopperThread(monitored, this::terminate).start(); } } private void terminate() { lifecycle.tryToMoveTo(STOPPED); hardStopWatcher.stopWatching(); stopWatcher.stopWatching(); commands.endWatch(); } public static ProcessEntryPoint createForArguments(String[] args) { Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args); File sharedDir = getSharedDir(props); int processNumber = getProcessNumber(props); ProcessCommands commands = DefaultProcessCommands.main(sharedDir, processNumber); return new ProcessEntryPoint(props, new SystemExit(), commands, Runtime.getRuntime()); } private static int getProcessNumber(Props props) { return Integer.parseInt(props.nonNullValue(PROPERTY_PROCESS_INDEX)); } private static File getSharedDir(Props props) { return props.nonNullValueAsFile(PROPERTY_SHARED_PATH); } /** * This watchdog is looking for hard stop to be requested via {@link ProcessCommands#askedForHardStop()}. */ private static StopWatcher createHardStopWatcher(ProcessCommands commands, ProcessEntryPoint processEntryPoint) { return new StopWatcher("HardStop Watcher", processEntryPoint::hardStopAsync, commands::askedForHardStop); } /** * This watchdog is looking for graceful stop to be requested via {@link ProcessCommands#askedForStop()} ()}. */ private static StopWatcher createStopWatcher(ProcessCommands commands, ProcessEntryPoint processEntryPoint) { return new StopWatcher("Stop Watcher", processEntryPoint::stopAsync, commands::askedForStop); } /** * Stops process in a graceful fashion */ private static class StopperThread extends AbstractStopperThread { private StopperThread(Monitored monitored, Runnable postAction, long terminationTimeoutMs) { super("Stopper", () -> { monitored.stop(); postAction.run(); }, terminationTimeoutMs); } } /** * Stops process in a short time fashion */ private static class HardStopperThread extends AbstractStopperThread { private HardStopperThread(Monitored monitored, Runnable postAction) { super( "HardStopper", () -> { monitored.hardStop(); postAction.run(); }, HARD_STOP_TIMEOUT_MS); } } }