001/*
002 * VM-Operator
003 * Copyright (C) 2023 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.logging.Level;
026import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetDisplayPassword;
027import org.jdrupes.vmoperator.runner.qemu.commands.QmpSetPasswordExpiry;
028import org.jdrupes.vmoperator.runner.qemu.events.ConfigureQemu;
029import org.jdrupes.vmoperator.runner.qemu.events.MonitorCommand;
030import org.jdrupes.vmoperator.runner.qemu.events.RunnerStateChange.RunState;
031import org.jgrapes.core.Channel;
032import org.jgrapes.core.Component;
033import org.jgrapes.core.annotation.Handler;
034import org.jgrapes.util.events.FileChanged;
035import org.jgrapes.util.events.WatchFile;
036
037/**
038 * The Class DisplayController.
039 */
040@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
041public class DisplayController extends Component {
042
043    public static final String DISPLAY_PASSWORD_FILE = "display-password";
044    public static final String PASSWORD_EXPIRY_FILE = "password-expiry";
045    private String currentPassword;
046    private String protocol;
047    private final Path configDir;
048
049    /**
050     * Instantiates a new Display controller.
051     *
052     * @param componentChannel the component channel
053     * @param configDir 
054     */
055    @SuppressWarnings({ "PMD.AssignmentToNonFinalStatic",
056        "PMD.ConstructorCallsOverridableMethod" })
057    public DisplayController(Channel componentChannel, Path configDir) {
058        super(componentChannel);
059        this.configDir = configDir;
060        fire(new WatchFile(configDir.resolve(DISPLAY_PASSWORD_FILE)));
061    }
062
063    /**
064     * On configure qemu.
065     *
066     * @param event the event
067     */
068    @Handler
069    public void onConfigureQemu(ConfigureQemu event) {
070        if (event.runState() == RunState.TERMINATING) {
071            return;
072        }
073        protocol
074            = event.configuration().vm.display.spice != null ? "spice" : null;
075        updatePassword();
076    }
077
078    /**
079     * Watch for changes of the password file.
080     *
081     * @param event the event
082     */
083    @Handler
084    @SuppressWarnings("PMD.EmptyCatchBlock")
085    public void onFileChanged(FileChanged event) {
086        if (event.path().equals(configDir.resolve(DISPLAY_PASSWORD_FILE))) {
087            updatePassword();
088        }
089    }
090
091    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
092    private void updatePassword() {
093        if (protocol == null) {
094            return;
095        }
096        if (setDisplayPassword()) {
097            setPasswordExpiry();
098        }
099    }
100
101    private boolean setDisplayPassword() {
102        String password;
103        Path dpPath = configDir.resolve(DISPLAY_PASSWORD_FILE);
104        if (dpPath.toFile().canRead()) {
105            logger.finer(() -> "Found display password");
106            try {
107                password = Files.readString(dpPath);
108            } catch (IOException e) {
109                logger.log(Level.WARNING, e, () -> "Cannot read display"
110                    + " password: " + e.getMessage());
111                return false;
112            }
113        } else {
114            logger.finer(() -> "No display password");
115            return false;
116        }
117
118        if (Objects.equals(this.currentPassword, password)) {
119            return true;
120        }
121        this.currentPassword = password;
122        logger.fine(() -> "Updating display password");
123        fire(new MonitorCommand(new QmpSetDisplayPassword(protocol, password)));
124        return true;
125    }
126
127    private void setPasswordExpiry() {
128        Path pePath = configDir.resolve(PASSWORD_EXPIRY_FILE);
129        if (!pePath.toFile().canRead()) {
130            return;
131        }
132        logger.finer(() -> "Found expiry time");
133        String expiry;
134        try {
135            expiry = Files.readString(pePath);
136        } catch (IOException e) {
137            logger.log(Level.WARNING, e, () -> "Cannot read expiry"
138                + " time: " + e.getMessage());
139            return;
140        }
141        logger.fine(() -> "Updating expiry time");
142        fire(new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry)));
143    }
144
145}