001/*
002 * VM-Operator
003 * Copyright (C) 2025 Michael N. Lipp
004 * 
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.vmoperator.runner.qemu;
020
021import com.fasterxml.jackson.core.JsonProcessingException;
022import com.fasterxml.jackson.databind.node.ObjectNode;
023import java.io.IOException;
024import java.time.Instant;
025import java.util.LinkedList;
026import java.util.Queue;
027import java.util.logging.Level;
028import org.jdrupes.vmoperator.runner.qemu.Constants.ProcessName;
029import org.jdrupes.vmoperator.runner.qemu.commands.QmpCommand;
030import org.jdrupes.vmoperator.runner.qemu.commands.QmpGuestGetOsinfo;
031import org.jdrupes.vmoperator.runner.qemu.commands.QmpGuestPowerdown;
032import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
033import org.jdrupes.vmoperator.runner.qemu.events.GuestAgentCommand;
034import org.jdrupes.vmoperator.runner.qemu.events.OsinfoEvent;
035import org.jgrapes.core.Channel;
036import org.jgrapes.core.Components;
037import org.jgrapes.core.Components.Timer;
038import org.jgrapes.core.annotation.Handler;
039import org.jgrapes.core.events.Stop;
040import org.jgrapes.io.events.ProcessExited;
041
042/**
043 * A component that handles the communication with the guest agent.
044 * 
045 * If the log level for this class is set to fine, the messages 
046 * exchanged on the monitor socket are logged.
047 */
048public class GuestAgentClient extends AgentConnector {
049
050    private boolean connected;
051    private Instant powerdownStartedAt;
052    private int powerdownTimeout;
053    private Timer powerdownTimer;
054    private final Queue<QmpCommand> executing = new LinkedList<>();
055    private Stop suspendedStop;
056
057    /**
058     * Instantiates a new guest agent client.
059     *
060     * @param componentChannel the component channel
061     * @throws IOException Signals that an I/O exception has occurred.
062     */
063    public GuestAgentClient(Channel componentChannel) throws IOException {
064        super(componentChannel);
065    }
066
067    /**
068     * When the agent has connected, request the OS information.
069     */
070    @Override
071    protected void agentConnected() {
072        logger.fine(() -> "guest agent connected");
073        connected = true;
074        rep().fire(new GuestAgentCommand(new QmpGuestGetOsinfo()));
075    }
076
077    @Override
078    protected void agentDisconnected() {
079        logger.fine(() -> "guest agent disconnected");
080        connected = false;
081    }
082
083    /**
084     * Process agent input.
085     *
086     * @param line the line
087     * @throws IOException Signals that an I/O exception has occurred.
088     */
089    @Override
090    protected void processInput(String line) throws IOException {
091        logger.fine(() -> "guest agent(in): " + line);
092        try {
093            var response = mapper.readValue(line, ObjectNode.class);
094            if (response.has("return") || response.has("error")) {
095                QmpCommand executed = executing.poll();
096                logger.fine(() -> String.format("(Previous \"guest agent(in)\""
097                    + " is result from executing %s)", executed));
098                if (executed instanceof QmpGuestGetOsinfo) {
099                    var osInfo = new OsinfoEvent(response.get("return"));
100                    rep().fire(osInfo);
101                }
102            }
103        } catch (JsonProcessingException e) {
104            throw new IOException(e);
105        }
106    }
107
108    /**
109     * On guest agent command.
110     *
111     * @param event the event
112     * @throws IOException Signals that an I/O exception has occurred.
113     */
114    @Handler
115    @SuppressWarnings({ "PMD.AvoidSynchronizedStatement",
116        "PMD.AvoidDuplicateLiterals" })
117    public void onGuestAgentCommand(GuestAgentCommand event)
118            throws IOException {
119        if (qemuChannel() == null) {
120            return;
121        }
122        var command = event.command();
123        logger.fine(() -> "guest agent(out): " + command.toString());
124        String asText;
125        try {
126            asText = command.asText();
127        } catch (JsonProcessingException e) {
128            logger.log(Level.SEVERE, e,
129                () -> "Cannot serialize Json: " + e.getMessage());
130            return;
131        }
132        synchronized (executing) {
133            if (writer().isPresent()) {
134                executing.add(command);
135                sendCommand(asText);
136            }
137        }
138    }
139
140    /**
141     * Shutdown the VM.
142     *
143     * @param event the event
144     */
145    @Handler(priority = 200)
146    @SuppressWarnings("PMD.AvoidSynchronizedStatement")
147    public void onStop(Stop event) {
148        if (!connected) {
149            logger.fine(() -> "No guest agent connection,"
150                + " cannot send shutdown command");
151            return;
152        }
153
154        // We have a connection to the guest agent attempt shutdown.
155        powerdownStartedAt = event.associated(Instant.class).orElseGet(() -> {
156            var now = Instant.now();
157            event.setAssociated(Instant.class, now);
158            return now;
159        });
160        var waitUntil = powerdownStartedAt.plusSeconds(powerdownTimeout);
161        if (waitUntil.isBefore(Instant.now())) {
162            return;
163        }
164        event.suspendHandling();
165        suspendedStop = event;
166        logger.fine(() -> "Sending powerdown command, waiting for"
167            + " termination until " + waitUntil);
168        powerdownTimer = Components.schedule(t -> {
169            logger.fine(() -> "Powerdown timeout reached.");
170            synchronized (this) {
171                powerdownTimer = null;
172                if (suspendedStop != null) {
173                    suspendedStop.resumeHandling();
174                    suspendedStop = null;
175                }
176            }
177        }, waitUntil);
178        rep().fire(new GuestAgentCommand(new QmpGuestPowerdown()));
179    }
180
181    /**
182     * On process exited.
183     *
184     * @param event the event
185     */
186    @Handler
187    @SuppressWarnings("PMD.AvoidSynchronizedStatement")
188    public void onProcessExited(ProcessExited event) {
189        if (!event.startedBy().associated(CommandDefinition.class)
190            .map(cd -> ProcessName.QEMU.equals(cd.name())).orElse(false)) {
191            return;
192        }
193        synchronized (this) {
194            if (powerdownTimer != null) {
195                powerdownTimer.cancel();
196            }
197            if (suspendedStop != null) {
198                suspendedStop.resumeHandling();
199                suspendedStop = null;
200            }
201        }
202    }
203
204    /**
205     * On configure qemu.
206     *
207     * @param event the event
208     */
209    @Handler
210    @SuppressWarnings("PMD.AvoidSynchronizedStatement")
211    public void onConfigureQemu(ConfigureQemu event) {
212        int newTimeout = event.configuration().vm.powerdownTimeout;
213        if (powerdownTimeout != newTimeout) {
214            powerdownTimeout = newTimeout;
215            synchronized (this) {
216                if (powerdownTimer != null) {
217                    powerdownTimer
218                        .reschedule(powerdownStartedAt.plusSeconds(newTimeout));
219                }
220
221            }
222        }
223    }
224}