001/*
002 * VM-Operator
003 * Copyright (C) 2023,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 java.io.IOException;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.util.Objects;
025import java.util.Optional;
026import java.util.logging.Level;
027import org.jdrupes.vmoperator.common.Constants.DisplaySecret;
028import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword;
029import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetPasswordExpiry;
030import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
031import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
032import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
033import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentConnected;
034import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogIn;
035import org.jdrupes.vmoperator.runner.qemu.events.VmopAgentLogOut;
036import org.jgrapes.core.Channel;
037import org.jgrapes.core.Component;
038import org.jgrapes.core.Event;
039import org.jgrapes.core.annotation.Handler;
040import org.jgrapes.util.events.FileChanged;
041import org.jgrapes.util.events.WatchFile;
042
043/**
044 * The Class DisplayController.
045 */
046@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
047public class DisplayController extends Component {
048
049    private String currentPassword;
050    private String protocol;
051    private final Path configDir;
052    private boolean vmopAgentConnected;
053    private String loggedInUser;
054
055    /**
056     * Instantiates a new Display controller.
057     *
058     * @param componentChannel the component channel
059     * @param configDir 
060     */
061    @SuppressWarnings({ "PMD.AssignmentToNonFinalStatic",
062        "PMD.ConstructorCallsOverridableMethod" })
063    public DisplayController(Channel componentChannel, Path configDir) {
064        super(componentChannel);
065        this.configDir = configDir;
066        fire(new WatchFile(configDir.resolve(DisplaySecret.PASSWORD)));
067    }
068
069    /**
070     * On configure qemu.
071     *
072     * @param event the event
073     */
074    @Handler
075    public void onConfigureQemu(ConfigureQemu event) {
076        if (event.runState() == RunState.TERMINATING) {
077            return;
078        }
079        protocol
080            = event.configuration().vm.display.spice != null ? "spice" : null;
081        loggedInUser = event.configuration().vm.display.loggedInUser;
082        configureLogin();
083        if (event.runState() == RunState.STARTING) {
084            configurePassword();
085        }
086    }
087
088    /**
089     * On vmop agent connected.
090     *
091     * @param event the event
092     */
093    @Handler
094    public void onVmopAgentConnected(VmopAgentConnected event) {
095        vmopAgentConnected = true;
096        configureLogin();
097    }
098
099    private void configureLogin() {
100        if (!vmopAgentConnected) {
101            return;
102        }
103        Event<?> evt = loggedInUser != null
104            ? new VmopAgentLogIn(loggedInUser)
105            : new VmopAgentLogOut();
106        fire(evt);
107    }
108
109    /**
110     * Watch for changes of the password file.
111     *
112     * @param event the event
113     */
114    @Handler
115    @SuppressWarnings("PMD.EmptyCatchBlock")
116    public void onFileChanged(FileChanged event) {
117        if (event.path().equals(configDir.resolve(DisplaySecret.PASSWORD))) {
118            configurePassword();
119        }
120    }
121
122    private void configurePassword() {
123        if (protocol == null) {
124            return;
125        }
126        if (setDisplayPassword()) {
127            setPasswordExpiry();
128        }
129    }
130
131    private boolean setDisplayPassword() {
132        return readFromFile(DisplaySecret.PASSWORD).map(password -> {
133            if (Objects.equals(this.currentPassword, password)) {
134                return true;
135            }
136            this.currentPassword = password;
137            logger.fine(() -> "Updating display password");
138            fire(new MonitorCommand(
139                new QmpSetDisplayPassword(protocol, password)));
140            return true;
141        }).orElse(false);
142    }
143
144    private void setPasswordExpiry() {
145        readFromFile(DisplaySecret.EXPIRY).ifPresent(expiry -> {
146            logger.fine(() -> "Updating expiry time to " + expiry);
147            fire(
148                new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry)));
149        });
150    }
151
152    private Optional<String> readFromFile(String dataItem) {
153        Path path = configDir.resolve(dataItem);
154        String label = dataItem.replace('-', ' ');
155        if (path.toFile().canRead()) {
156            logger.finer(() -> "Found " + label);
157            try {
158                return Optional.ofNullable(Files.readString(path));
159            } catch (IOException e) {
160                logger.log(Level.WARNING, e, () -> "Cannot read " + label + ": "
161                    + e.getMessage());
162                return Optional.empty();
163            }
164        } else {
165            logger.finer(() -> "No " + label);
166            return Optional.empty();
167        }
168    }
169}