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 */
046public class DisplayController extends Component {
047
048    private String currentPassword;
049    private String protocol;
050    private final Path configDir;
051    private boolean canBeUpdated;
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.ConstructorCallsOverridableMethod" })
062    public DisplayController(Channel componentChannel, Path configDir) {
063        super(componentChannel);
064        this.configDir = configDir;
065        fire(new WatchFile(configDir.resolve(DisplaySecret.PASSWORD)));
066    }
067
068    /**
069     * On configure qemu.
070     *
071     * @param event the event
072     */
073    @Handler
074    public void onConfigureQemu(ConfigureQemu event) {
075        if (event.runState() == RunState.TERMINATING) {
076            return;
077        }
078        protocol
079            = event.configuration().vm.display.spice != null ? "spice" : null;
080        loggedInUser = event.configuration().vm.display.loggedInUser;
081        configureLogin();
082        if (event.runState() == RunState.STARTING) {
083            configurePassword();
084        }
085        canBeUpdated = true;
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    public void onFileChanged(FileChanged event) {
116        if (event.path().equals(configDir.resolve(DisplaySecret.PASSWORD))) {
117            logger.fine(() -> "Display password updated");
118            if (canBeUpdated) {
119                configurePassword();
120            }
121        }
122    }
123
124    private void configurePassword() {
125        if (protocol == null) {
126            return;
127        }
128        if (setDisplayPassword()) {
129            setPasswordExpiry();
130        }
131    }
132
133    private boolean setDisplayPassword() {
134        return readFromFile(DisplaySecret.PASSWORD).map(password -> {
135            if (Objects.equals(this.currentPassword, password)) {
136                return true;
137            }
138            this.currentPassword = password;
139            logger.fine(() -> "Updating display password");
140            fire(new MonitorCommand(
141                new QmpSetDisplayPassword(protocol, password)));
142            return true;
143        }).orElse(false);
144    }
145
146    private void setPasswordExpiry() {
147        readFromFile(DisplaySecret.EXPIRY).ifPresent(expiry -> {
148            logger.fine(() -> "Updating expiry time to " + expiry);
149            fire(
150                new MonitorCommand(new QmpSetPasswordExpiry(protocol, expiry)));
151        });
152    }
153
154    private Optional<String> readFromFile(String dataItem) {
155        Path path = configDir.resolve(dataItem);
156        String label = dataItem.replace('-', ' ');
157        if (path.toFile().canRead()) {
158            logger.finer(() -> "Found " + label);
159            try {
160                return Optional.ofNullable(Files.readString(path));
161            } catch (IOException e) {
162                logger.log(Level.WARNING, e, () -> "Cannot read " + label + ": "
163                    + e.getMessage());
164                return Optional.empty();
165            }
166        } else {
167            logger.finer(() -> "No " + label);
168            return Optional.empty();
169        }
170    }
171}