Refactor and implement shutdown.

This commit is contained in:
Michael Lipp 2023-06-04 13:05:02 +02:00
parent 0a6cb82516
commit 7a9ed262c1
9 changed files with 493 additions and 188 deletions

View file

@ -10,7 +10,7 @@ plugins {
dependencies { dependencies {
implementation 'org.jgrapes:org.jgrapes.core:[1.19.0,2)' implementation 'org.jgrapes:org.jgrapes.core:[1.19.0,2)'
implementation 'org.jgrapes:org.jgrapes.io:[2.5.0,3)' implementation 'org.jgrapes:org.jgrapes.io:[2.7.0,3)'
implementation 'org.jgrapes:org.jgrapes.http:[3.1.0,4)' implementation 'org.jgrapes:org.jgrapes.http:[3.1.0,4)'
implementation 'org.jgrapes:org.jgrapes.util:[1.28.0,2)' implementation 'org.jgrapes:org.jgrapes.util:[1.28.0,2)'

View file

@ -18,7 +18,6 @@
package org.jdrupes.vmoperator.runner.qemu; package org.jdrupes.vmoperator.runner.qemu;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
@ -46,7 +45,6 @@ class Configuration implements Dto {
public Path monitorSocket; public Path monitorSocket;
public Path firmwareRom; public Path firmwareRom;
public Path firmwareFlash; public Path firmwareFlash;
public JsonNode monitorMessages;
@SuppressWarnings("PMD.ShortVariable") @SuppressWarnings("PMD.ShortVariable")
public Vm vm; public Vm vm;
@ -126,10 +124,8 @@ class Configuration implements Dto {
} }
} }
runtimeDir += "/vmrunner/" + vm.name; runtimeDir += "/vmrunner/" + vm.name;
swtpmSocket swtpmSocket = Path.of(runtimeDir, "swtpm-sock");
= Path.of(runtimeDir, "swtpm-sock"); monitorSocket = Path.of(runtimeDir, "monitor.sock");
monitorSocket
= Path.of(runtimeDir, "monitor.sock");
} }
Path runtimePath = Path.of(runtimeDir); Path runtimePath = Path.of(runtimeDir);
if (!Files.exists(runtimePath)) { if (!Files.exists(runtimePath)) {

View file

@ -0,0 +1,251 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.runner.qemu;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.UnixDomainSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.core.Components;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.Start;
import org.jgrapes.core.events.Stop;
import org.jgrapes.io.events.Closed;
import org.jgrapes.io.events.ConnectError;
import org.jgrapes.io.events.Input;
import org.jgrapes.io.events.OpenSocketConnection;
import org.jgrapes.io.util.ByteBufferWriter;
import org.jgrapes.io.util.LineCollector;
import org.jgrapes.net.SocketIOChannel;
import org.jgrapes.net.events.ClientConnected;
import org.jgrapes.util.events.ConfigurationUpdate;
import org.jgrapes.util.events.FileChanged;
import org.jgrapes.util.events.FileChanged.Kind;
import org.jgrapes.util.events.WatchFile;
/**
* A component that handles the communication over the Qemu monitor
* socket.
*/
public class QemuMonitor extends Component {
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
private static final Logger monitorLog
= Logger.getLogger(QemuMonitor.class.getName());
@SuppressWarnings("PMD.UseConcurrentHashMap")
private final Map<String, String> monitorMessages = new HashMap<>(Map.of(
"connect", "{ \"execute\": \"qmp_capabilities\" }",
"powerdown", "{ \"execute\": \"system_powerdown\" }"));
private Path socketPath;
private SocketIOChannel monitorChannel;
private Stop suspendedStop;
/**
* Instantiates a new qemu monitor.
*
* @param componentChannel the component channel
*/
public QemuMonitor(Channel componentChannel) {
super(componentChannel);
}
/**
* As the configuration of this component depends on the configuration
* of the {@link Runner}, it doesn't have a handler for the
* {@link ConfigurationUpdate} event. The values are forwarded from the
* {@link Runner} instead.
*
* @param socketPath the socket path
*/
/* default */ void configure(Path socketPath) {
this.socketPath = socketPath;
}
/**
* Handle the start event.
*
* @param event the event
* @throws IOException Signals that an I/O exception has occurred.
*/
@Handler
public void onStart(Start event) throws IOException {
Files.deleteIfExists(socketPath);
fire(new WatchFile(socketPath));
}
/**
* Watch for the creation of the swtpm socket and start the
* qemu process if it has been created.
*
* @param event the event
* @param context the context
*/
@Handler
public void onFileChanged(FileChanged event) {
if (event.change() == Kind.CREATED && event.path().equals(socketPath)) {
// qemu running, open socket
fire(new OpenSocketConnection(
UnixDomainSocketAddress.of(socketPath))
.setAssociated(QemuMonitor.class, this));
}
}
/**
* Check if this is from opening the monitor socket and if true,
* save the socket in the context and associate the channel with
* the context. Then send the initial message to the socket.
*
* @param event the event
* @param channel the channel
*/
@Handler
public void onClientConnected(ClientConnected event,
SocketIOChannel channel) {
event.openEvent().associated(QemuMonitor.class).ifPresent(qm -> {
monitorChannel = channel;
channel.setAssociated(QemuMonitor.class, this);
channel.setAssociated(Writer.class, new ByteBufferWriter(
channel).nativeCharset());
channel.setAssociated(LineCollector.class,
new LineCollector()
.consumer(line -> {
try {
processMonitorInput(line);
} catch (IOException e) {
throw new UndeclaredThrowableException(e);
}
}));
writeToMonitor(monitorMessages.get("connect"));
});
}
/**
* Called when a connection attempt fails.
*
* @param event the event
* @param channel the channel
*/
@Handler
public void onConnectError(ConnectError event, SocketIOChannel channel) {
event.event().associated(QemuMonitor.class).ifPresent(qm -> {
fire(new Stop());
});
}
private void writeToMonitor(String message) {
monitorLog.fine(() -> "monitor(out): " + message);
monitorChannel.associated(Writer.class).ifPresent(writer -> {
try {
writer.append(message).append('\n').flush();
} catch (IOException e) {
// Cannot happen, but...
logger.log(Level.WARNING, e, () -> e.getMessage());
}
});
}
/**
* Handle data from qemu monitor connection.
*
* @param event the event
* @param channel the channel
*/
@Handler
public void onInput(Input<?> event, SocketIOChannel channel) {
if (channel.associated(QemuMonitor.class).isEmpty()) {
return;
}
channel.associated(LineCollector.class).ifPresent(collector -> {
collector.feed(event);
});
}
private void processMonitorInput(String line)
throws IOException {
monitorLog.fine(() -> "monitor(in): " + line);
try {
var response
= ((Runner) channel()).mapper().readValue(line, JsonNode.class);
if (response.has("QMP")) {
fire(new QemuMonitorOpened());
}
} catch (JsonProcessingException e) {
throw new IOException(e);
}
}
/**
* On closed.
*
* @param event the event
*/
@Handler
public void onClosed(Closed<?> event, SocketIOChannel channel) {
channel.associated(QemuMonitor.class).ifPresent(qm -> {
monitorChannel = null;
synchronized (this) {
if (suspendedStop != null) {
suspendedStop.resumeHandling();
suspendedStop = null;
}
}
});
}
/**
* Shutdown the VM.
*
* @param event the event
*/
@Handler(priority = 100)
public void onStop(Stop event) {
if (monitorChannel != null) {
// We have a connection to Qemu, attempt ACPI shutdown.
event.suspendHandling();
suspendedStop = event;
writeToMonitor(monitorMessages.get("powerdown"));
// Schedule timer as fallback
Components.schedule(t -> {
synchronized (this) {
if (suspendedStop != null) {
suspendedStop.resumeHandling();
suspendedStop = null;
}
}
}, Duration.ofMillis(5000));
}
}
}

View file

@ -0,0 +1,29 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.runner.qemu;
import org.jgrapes.io.events.Opened;
/**
* Signals that the connection to the Qemu monitor socket has been
* established successfully.
*/
public class QemuMonitorOpened extends Opened<Void> {
}

View file

@ -33,17 +33,18 @@ import java.io.File;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.io.Writer; import java.lang.reflect.UndeclaredThrowableException;
import java.net.UnixDomainSocketAddress;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import static org.jdrupes.vmoperator.runner.qemu.Configuration.BOOT_MODE_SECURE; import static org.jdrupes.vmoperator.runner.qemu.Configuration.BOOT_MODE_SECURE;
import static org.jdrupes.vmoperator.runner.qemu.Configuration.BOOT_MODE_UEFI; import static org.jdrupes.vmoperator.runner.qemu.Configuration.BOOT_MODE_UEFI;
import org.jdrupes.vmoperator.runner.qemu.StateController.State;
import org.jdrupes.vmoperator.util.ExtendedObjectWrapper; import org.jdrupes.vmoperator.util.ExtendedObjectWrapper;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
@ -51,36 +52,34 @@ import org.jgrapes.core.Components;
import org.jgrapes.core.TypedIdKey; import org.jgrapes.core.TypedIdKey;
import org.jgrapes.core.annotation.Handler; import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.events.Start; import org.jgrapes.core.events.Start;
import org.jgrapes.core.events.Started;
import org.jgrapes.core.events.Stop; import org.jgrapes.core.events.Stop;
import org.jgrapes.io.NioDispatcher; import org.jgrapes.io.NioDispatcher;
import org.jgrapes.io.events.ConnectError;
import org.jgrapes.io.events.Input; import org.jgrapes.io.events.Input;
import org.jgrapes.io.events.OpenSocketConnection;
import org.jgrapes.io.events.ProcessExited; import org.jgrapes.io.events.ProcessExited;
import org.jgrapes.io.events.ProcessStarted; import org.jgrapes.io.events.ProcessStarted;
import org.jgrapes.io.events.StartProcess; import org.jgrapes.io.events.StartProcess;
import org.jgrapes.io.process.ProcessManager; import org.jgrapes.io.process.ProcessManager;
import org.jgrapes.io.process.ProcessManager.ProcessChannel; import org.jgrapes.io.process.ProcessManager.ProcessChannel;
import org.jgrapes.io.util.ByteBufferWriter;
import org.jgrapes.io.util.LineCollector; import org.jgrapes.io.util.LineCollector;
import org.jgrapes.net.SocketConnector; import org.jgrapes.net.SocketConnector;
import org.jgrapes.net.SocketIOChannel;
import org.jgrapes.net.events.ClientConnected;
import org.jgrapes.util.FileSystemWatcher; import org.jgrapes.util.FileSystemWatcher;
import org.jgrapes.util.YamlConfigurationStore; import org.jgrapes.util.YamlConfigurationStore;
import org.jgrapes.util.events.ConfigurationUpdate; import org.jgrapes.util.events.ConfigurationUpdate;
import org.jgrapes.util.events.FileChanged; import org.jgrapes.util.events.FileChanged;
import org.jgrapes.util.events.FileChanged.Kind; import org.jgrapes.util.events.FileChanged.Kind;
import org.jgrapes.util.events.InitialConfiguration;
import org.jgrapes.util.events.WatchFile; import org.jgrapes.util.events.WatchFile;
/** /**
* The Runner. * The Runner.
* *
* @startuml * @startuml
* [*] --> Setup * [*] --> Initializing
* Setup --> Setup: InitialConfiguration/configure Runner * Initializing -> Initializing: InitialConfiguration/configure Runner
* Initializing -> Initializing: Start/start Runner
* *
* state Startup { * state "Starting (Processes)" as StartingProcess {
* *
* state which <<choice>> * state which <<choice>>
* state "Start swtpm" as swtpm * state "Start swtpm" as swtpm
@ -105,9 +104,9 @@ import org.jgrapes.util.events.WatchFile;
* monitor --> error: ConnectError[for monitor] * monitor --> error: ConnectError[for monitor]
* } * }
* *
* Setup --> which: Start * Initializing --> which: Started
* *
* success --> Run * success --> Running
* error --> [*] * error --> [*]
* *
* @enduml * @enduml
@ -124,18 +123,15 @@ public class Runner extends Component {
private static final String SAVED_TEMPLATE = "VM.ftl.yaml"; private static final String SAVED_TEMPLATE = "VM.ftl.yaml";
private static final String FW_FLASH = "fw-flash.fd"; private static final String FW_FLASH = "fw-flash.fd";
@SuppressWarnings({ "PMD.FieldNamingConventions",
"PMD.VariableNamingConventions" })
private static final Logger monitorLog
= Logger.getLogger(Runner.class.getPackageName() + ".monitor");
private static Runner app;
private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
private final JsonNode defaults; private final JsonNode defaults;
@SuppressWarnings("PMD.UseConcurrentHashMap") @SuppressWarnings("PMD.UseConcurrentHashMap")
private Configuration config = new Configuration(); private Configuration config = new Configuration();
private final freemarker.template.Configuration fmConfig; private final freemarker.template.Configuration fmConfig;
private final StateController state;
private CommandDefinition swtpmDefinition;
private CommandDefinition qemuDefinition;
private final QemuMonitor qemuMonitor;
/** /**
* Instantiates a new runner. * Instantiates a new runner.
@ -143,7 +139,7 @@ public class Runner extends Component {
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
*/ */
public Runner() throws IOException { public Runner() throws IOException {
super(new Context()); state = new StateController(this);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false); false);
@ -167,6 +163,7 @@ public class Runner extends Component {
attach(new FileSystemWatcher(channel())); attach(new FileSystemWatcher(channel()));
attach(new ProcessManager(channel())); attach(new ProcessManager(channel()));
attach(new SocketConnector(channel())); attach(new SocketConnector(channel()));
attach(qemuMonitor = new QemuMonitor(channel()));
// Configuration store with file in /etc (default) // Configuration store with file in /etc (default)
File config = new File(System.getProperty( File config = new File(System.getProperty(
@ -176,6 +173,10 @@ public class Runner extends Component {
fire(new WatchFile(config.toPath())); fire(new WatchFile(config.toPath()));
} }
/* default */ ObjectMapper mapper() {
return mapper;
}
/** /**
* On configuration update. * On configuration update.
* *
@ -184,58 +185,64 @@ public class Runner extends Component {
@Handler @Handler
public void onConfigurationUpdate(ConfigurationUpdate event) { public void onConfigurationUpdate(ConfigurationUpdate event) {
event.structured(componentPath()).ifPresent(c -> { event.structured(componentPath()).ifPresent(c -> {
try { if (event instanceof InitialConfiguration) {
config = mapper.convertValue(c, Configuration.class); processInitialConfiguration(c);
} catch (IllegalArgumentException e) {
logger.log(Level.SEVERE, e, () -> "Invalid configuration: "
+ e.getMessage());
// Don't use default configuration
config = null;
} }
}); });
} }
private void processInitialConfiguration(
Map<String, Object> runnerConfiguration) {
try {
config = mapper.convertValue(runnerConfiguration,
Configuration.class);
if (!config.check()) {
// Invalid configuration, not used, problems already logged.
config = null;
}
// Forward some values to child components
qemuMonitor.configure(config.monitorSocket);
} catch (IllegalArgumentException e) {
logger.log(Level.SEVERE, e, () -> "Invalid configuration: "
+ e.getMessage());
// Don't use default configuration
config = null;
}
}
/** /**
* Handle the start event. * Handle the start event.
* *
* @param event the event * @param event the event
*/ */
@Handler @Handler
@SuppressWarnings({ "PMD.SystemPrintln" })
public void onStart(Start event) { public void onStart(Start event) {
try { try {
if (config == null || !config.check()) { if (config == null) {
// Invalid configuration, fail // Missing configuration, fail
fire(new Stop()); fire(new Stop());
return; return;
} }
// Store process id
try (var pidFile = Files.newBufferedWriter(
Path.of(config.runtimeDir, "runner.pid"))) {
pidFile.write(ProcessHandle.current().pid() + "\n");
}
// Prepare firmware files and add to config // Prepare firmware files and add to config
setFirmwarePaths(); setFirmwarePaths();
// Obtain more data from template // Obtain more context data from template
var tplData = dataFromTemplate(); var tplData = dataFromTemplate();
swtpmDefinition = Optional.ofNullable(tplData.get("swtpm"))
// Get process definitions etc. from processed data
Context context = (Context) channel();
context.swtpmDefinition = Optional.ofNullable(tplData.get("swtpm"))
.map(d -> new CommandDefinition("swtpm", d)).orElse(null); .map(d -> new CommandDefinition("swtpm", d)).orElse(null);
context.qemuDefinition = Optional.ofNullable(tplData.get("qemu")) qemuDefinition = Optional.ofNullable(tplData.get("qemu"))
.map(d -> new CommandDefinition("qemu", d)).orElse(null); .map(d -> new CommandDefinition("qemu", d)).orElse(null);
config.monitorMessages = tplData.get("monitorMessages");
// Files to watch for // Files to watch for
Files.deleteIfExists(config.swtpmSocket); Files.deleteIfExists(config.swtpmSocket);
fire(new WatchFile(config.swtpmSocket)); fire(new WatchFile(config.swtpmSocket));
Files.deleteIfExists(config.monitorSocket);
fire(new WatchFile(config.monitorSocket));
// Start first
if (config.vm.useTpm && context.swtpmDefinition != null) {
startProcess(context, context.swtpmDefinition);
return;
}
startProcess(context, context.qemuDefinition);
} catch (IOException | TemplateException e) { } catch (IOException | TemplateException e) {
logger.log(Level.SEVERE, e, logger.log(Level.SEVERE, e,
() -> "Cannot configure runner: " + e.getMessage()); () -> "Cannot configure runner: " + e.getMessage());
@ -307,12 +314,27 @@ public class Runner extends Component {
return mapper.readValue(out.toString(), JsonNode.class); return mapper.readValue(out.toString(), JsonNode.class);
} }
private boolean startProcess(Context context, CommandDefinition toStart) { /**
* Handle the started event.
*
* @param event the event
*/
@Handler
public void onStarted(Started event) {
state.set(State.STARTING);
// Start first process
if (config.vm.useTpm && swtpmDefinition != null) {
startProcess(swtpmDefinition);
return;
}
startProcess(qemuDefinition);
}
private boolean startProcess(CommandDefinition toStart) {
logger.fine( logger.fine(
() -> "Starting process: " + String.join(" ", toStart.command)); () -> "Starting process: " + String.join(" ", toStart.command));
fire(new StartProcess(toStart.command) fire(new StartProcess(toStart.command)
.setAssociated(Context.class, context) .setAssociated(CommandDefinition.class, toStart));
.setAssociated(CommandDefinition.class, toStart), channel());
return true; return true;
} }
@ -324,22 +346,13 @@ public class Runner extends Component {
* @param context the context * @param context the context
*/ */
@Handler @Handler
public void onFileChanged(FileChanged event, Context context) { public void onFileChanged(FileChanged event) {
if (event.change() == Kind.CREATED if (event.change() == Kind.CREATED
&& event.path() && event.path().equals(config.swtpmSocket)) {
.equals(Path.of(config.runtimeDir, "swtpm-sock"))) {
// swtpm running, start qemu // swtpm running, start qemu
startProcess(context, context.qemuDefinition); startProcess(qemuDefinition);
return; return;
} }
var monSockPath = Path.of(config.runtimeDir, "monitor.sock");
if (event.change() == Kind.CREATED
&& event.path().equals(monSockPath)) {
// qemu running, open socket
fire(new OpenSocketConnection(
UnixDomainSocketAddress.of(monSockPath))
.setAssociated(Context.class, context));
}
} }
/** /**
@ -355,34 +368,27 @@ public class Runner extends Component {
"PMD.TooFewBranchesForASwitchStatement" }) "PMD.TooFewBranchesForASwitchStatement" })
public void onProcessStarted(ProcessStarted event, ProcessChannel channel) public void onProcessStarted(ProcessStarted event, ProcessChannel channel)
throws InterruptedException { throws InterruptedException {
event.startEvent().associated(Context.class).ifPresent(context -> { event.startEvent().associated(CommandDefinition.class)
// Associate the process channel with the general context .ifPresent(procDef -> {
// and with its process definition (both carried over by channel.setAssociated(CommandDefinition.class, procDef);
// the start event). try (var pidFile = Files.newBufferedWriter(
channel.setAssociated(Context.class, context); Path.of(config.runtimeDir, procDef.name + ".pid"))) {
CommandDefinition procDef pidFile.write(channel.process().toHandle().pid() + "\n");
= event.startEvent().associated(CommandDefinition.class).get(); } catch (IOException e) {
channel.setAssociated(CommandDefinition.class, procDef); throw new UndeclaredThrowableException(e);
}
// Associate the channel with a line collector (one for // Associate the channel with a line collector (one for
// each stream) for logging the process's output. // each stream) for logging the process's output.
TypedIdKey.associate(channel, 1, new LineCollector().nativeCharset() TypedIdKey.associate(channel, 1,
.consumer(line -> logger new LineCollector().nativeCharset()
.info(() -> procDef.name() + "(out): " + line))); .consumer(line -> logger
TypedIdKey.associate(channel, 2, new LineCollector().nativeCharset() .info(() -> procDef.name() + "(out): " + line)));
.consumer(line -> logger TypedIdKey.associate(channel, 2,
.info(() -> procDef.name() + "(err): " + line))); new LineCollector().nativeCharset()
.consumer(line -> logger
// Register the channel in the context. .info(() -> procDef.name() + "(err): " + line)));
switch (procDef.name) { });
case "swtpm":
context.swtpmChannel = channel;
break;
case "qemu":
context.qemuChannel = channel;
break;
}
});
} }
/** /**
@ -399,16 +405,14 @@ public class Runner extends Component {
} }
/** /**
* Handle data from qemu monitor connection. * On qemu monitor started.
* *
* @param event the event * @param event the event
* @param channel the channel * @param context the context
*/ */
@Handler @Handler
public void onInput(Input<?> event, SocketIOChannel channel) { public void onQemuMonitorOpened(QemuMonitorOpened event) {
channel.associated(LineCollector.class).ifPresent(collector -> { state.set(State.RUNNING);
collector.feed(event);
});
} }
/** /**
@ -423,84 +427,19 @@ public class Runner extends Component {
} }
/** /**
* Check if this is from opening the monitor socket and if true, * On stop.
* save the socket in the context and associate the channel with
* the context. Then send the initial message to the socket.
* *
* @param event the event * @param event the event
* @param channel the channel
*/ */
@Handler @Handler
public void onClientConnected(ClientConnected event, public void onStop(Stop event) {
SocketIOChannel channel) { // Context context = (Context) channel();
if (event.openEvent().address() instanceof UnixDomainSocketAddress addr // if (context.qemuChannel != null) {
&& addr.getPath() // event.suspendHandling();
.equals(Path.of(config.runtimeDir, "monitor.sock"))) { // context.suspendedStop = event;
event.openEvent().associated(Context.class).ifPresent(context -> { // writeToMonitor(context,
context.monitorChannel = channel; // config.monitorMessages.get("powerdown").asText());
channel.setAssociated(Context.class, context); // }
channel.setAssociated(LineCollector.class,
new LineCollector().consumer(line -> {
monitorLog.fine(() -> "monitor(in): " + line);
}));
channel.setAssociated(Writer.class, new ByteBufferWriter(
channel).nativeCharset());
writeToMonitor(context,
config.monitorMessages.get("connect").asText());
});
}
}
/**
* Called when a connection attempt fails.
*
* @param event the event
* @param channel the channel
*/
@Handler
public void onConnectError(ConnectError event, SocketIOChannel channel) {
if (event.event() instanceof OpenSocketConnection openEvent
&& openEvent.address() instanceof UnixDomainSocketAddress addr
&& addr.getPath()
.equals(Path.of(config.runtimeDir, "monitor.sock"))) {
openEvent.associated(Context.class).ifPresent(context -> {
fire(new Stop());
});
}
}
private void writeToMonitor(Context context, String message) {
monitorLog.fine(() -> "monitor(out): " + message);
context.monitorChannel.associated(Writer.class)
.ifPresent(writer -> {
try {
writer.append(message).append('\n').flush();
} catch (IOException e) {
// Cannot happen, but...
logger.log(Level.WARNING, e, () -> e.getMessage());
}
});
}
/**
* The context.
*/
private static class Context implements Channel {
public CommandDefinition swtpmDefinition;
public CommandDefinition qemuDefinition;
public ProcessChannel swtpmChannel;
public ProcessChannel qemuChannel;
public SocketIOChannel monitorChannel;
@Override
public Object defaultCriterion() {
return "ProcMgr";
}
@Override
public String toString() {
return "ProcMgr";
}
} }
/** /**
@ -511,7 +450,7 @@ public class Runner extends Component {
public static void main(String[] args) { public static void main(String[] args) {
// The Runner is the root component // The Runner is the root component
try { try {
app = new Runner(); var app = new Runner();
// Prepare Stop // Prepare Stop
Runtime.getRuntime().addShutdownHook(new Thread(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {
@ -529,6 +468,5 @@ public class Runner extends Component {
Logger.getLogger(Runner.class.getName()).log(Level.SEVERE, e, Logger.getLogger(Runner.class.getName()).log(Level.SEVERE, e,
() -> "Failed to start runner: " + e.getMessage()); () -> "Failed to start runner: " + e.getMessage());
} }
} }
} }

View file

@ -0,0 +1,54 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.runner.qemu;
/**
* The context.
*/
/* default */ class StateController {
private Runner runner;
/**
* The state.
*/
enum State {
INITIALIZING, STARTING, RUNNING, TERMINATING
}
private State state = State.INITIALIZING;
public StateController(Runner runner) {
this.runner = runner;
}
/**
* Sets the state.
*
* @param state the new state
*/
public void set(State state) {
this.state = state;
}
@Override
public String toString() {
return "StateController [state=" + state + "]";
}
}

View file

@ -152,8 +152,3 @@
- [ "-chardev", "spicevmc,id=charredir${ index },name=usbredir" ] - [ "-chardev", "spicevmc,id=charredir${ index },name=usbredir" ]
- [ "-device", "usb-redir,id=redir${ index },chardev=charredir${ index }" ] - [ "-device", "usb-redir,id=redir${ index },chardev=charredir${ index }" ]
</#list> </#list>
"monitorMessages":
"connect": '{ "execute": "qmp_capabilities" }'

View file

@ -1,5 +1,5 @@
# #
#Sun May 28 16:23:45 CEST 2023 #Tue May 30 22:54:50 CEST 2023
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
@ -286,9 +286,9 @@ org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=1
org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines
org.eclipse.jdt.core.formatter.lineSplit=80 org.eclipse.jdt.core.formatter.lineSplit=80
org.eclipse.jdt.core.compiler.source=17 org.eclipse.jdt.core.compiler.source=17
org.eclipse.jdt.core.formatter.tabulation.size=4
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert
org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
org.eclipse.jdt.core.formatter.tabulation.size=4
org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
@ -304,13 +304,13 @@ org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_
org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
eclipse.preferences.version=1 eclipse.preferences.version=1
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
org.eclipse.jdt.core.compiler.compliance=17 org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.formatter.blank_lines_after_package=1 org.eclipse.jdt.core.formatter.blank_lines_after_package=1
org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16

View file

@ -0,0 +1,42 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.util;
import java.util.logging.LogManager;
public class LongLoggingManager extends LogManager {
private static LongLoggingManager instance;
public LongLoggingManager() {
instance = this;
}
@Override
public void reset() {
/* don't reset yet. */
}
private void reset0() {
super.reset();
}
public static void resetFinally() {
instance.reset0();
}
}