From 63567fe0ba50cf20c8be0eca2fbf21783bc415f1 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Wed, 11 Mar 2020 11:55:38 -0300 Subject: [PATCH] Use Linux OS suspend inhibitors to avoid standby On linux hosts, AvoidStandbyService checks for executables gnome-session-inhibit and systemd-inhibit in that order, then starts the first found to prevent suspend or sleep while Bisq is running. Although the hide avoid standby mode button is not displayed on linux, the inhibitors can be toggled off/on when and if that button is displayed. An installed shutdown hook will turn the inhibitor off, but they should always die when the parent process dies. To check when an inhibitor is enabled or disabled from a linux terminal: If the desktop is gnome... $ dbus-send --session --print-reply --dest=org.gnome.SessionManager \ /org/gnome/SessionManager org.gnome.SessionManager.GetInhibitors If Bisq falls back to systemd-inhibit on a non gnome desktop... $ systemd-inhibit --list $ systemd-inhibit --list | grep Bisq If neither gnome-session-inhibit nor systemd-inhibit are present, the standby service falls back to playing the silent audio file. This does not cover all linux desktop types, but other inhibitors exist for desktops I have not set up, such as mate, and it will not be difficult to make changes to support them. Memory use reduction varies from machine to (virtual) machine, but on my Ubuntu 18 laptop, playing the audio file to block suspension is allocating about 1 GB of native memory per hour -- via malloc() -- which this PR elminates. On the same machine, virtual memory use goes from 10.4 GB to 10.1 GB. This has been tested on Ubuntu 18 (gnome) and the following VMs: Debian 10.3 (gnome) Arch Linux 9.2.1 (gnome) Fedora 31.1.9 (cinnamon) But, VMs not running an inhibitor or audio file did not always go into suspend mode as expected. This does not work on Windows Cygwin, but minor changes might make it work if cygwin inhibitor tools like gnome-session-inhibit.exe and mate-session-inhibit.exe are installed. Addresses native / virtual memory allocation issues. --- .../core/app/AvoidStandbyModeService.java | 105 +++++++++++++++++- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/bisq/core/app/AvoidStandbyModeService.java b/core/src/main/java/bisq/core/app/AvoidStandbyModeService.java index 365e0f9a70c..96015d757dc 100644 --- a/core/src/main/java/bisq/core/app/AvoidStandbyModeService.java +++ b/core/src/main/java/bisq/core/app/AvoidStandbyModeService.java @@ -22,13 +22,23 @@ import bisq.common.config.Config; import bisq.common.storage.FileUtil; import bisq.common.storage.ResourceNotFoundException; +import bisq.common.util.Utilities; import javax.inject.Inject; import javax.inject.Singleton; +import java.nio.file.Paths; + import java.io.File; import java.io.IOException; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + import lombok.extern.slf4j.Slf4j; @@ -46,14 +56,16 @@ public class AvoidStandbyModeService { private final Preferences preferences; private final Config config; - + private final Optional inhibitorPathSpec; + private CountDownLatch cancelInhibitSuspendLatch; private volatile boolean isStopped; + @Inject public AvoidStandbyModeService(Preferences preferences, Config config) { this.preferences = preferences; this.config = config; - + this.inhibitorPathSpec = inhibitorPath(); preferences.getUseStandbyModeProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { isStopped = true; @@ -74,9 +86,55 @@ public void init() { private void start() { isStopped = false; log.info("AvoidStandbyModeService started"); - new Thread(this::play, "AvoidStandbyModeService-thread").start(); + if (Utilities.isLinux()) { + startInhibitor(); + new Thread(this::stopInhibitor, "StopAvoidStandbyModeService-thread").start(); + } else { + new Thread(this::play, "AvoidStandbyModeService-thread").start(); + } + } + + private void startInhibitor() { + try { + inhibitCommand().ifPresent(cmd -> { + try { + final Process process = new ProcessBuilder(cmd).start(); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + if (process.isAlive()) { + process.destroy(); + log.info("AvoidStandbyModeService shutdown"); + } + }, "AvoidStandbyModeService.ShutDownHook")); + cancelInhibitSuspendLatch = new CountDownLatch(1); + log.info("disabled power management via {}", String.join(" ", cmd)); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } catch (Exception e) { + log.error("could not use inhibitor to avoid standby mode", e); + } } + private void stopInhibitor() { + try { + if (!isStopped) { + Objects.requireNonNull(cancelInhibitSuspendLatch).await(); + } + inhibitorPathSpec.ifPresent(cmd -> { + Optional javaProcess = ProcessHandle.of(ProcessHandle.current().pid()); + javaProcess.ifPresent(process -> process.children().forEach(childProcess -> childProcess.info().command().ifPresent(command -> { + if (command.equals(cmd) && childProcess.isAlive()) { + childProcess.destroy(); + log.info("AvoidStandbyModeService stopped"); + } + }))); + }); + Objects.requireNonNull(cancelInhibitSuspendLatch).countDown(); + } catch (Exception e) { + log.error("stop inhibitor thread interrupted", e); + } + } private void play() { try { @@ -113,4 +171,45 @@ private SourceDataLine getSourceDataLine(AudioFormat audioFormat) throws LineUna DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, audioFormat); return (SourceDataLine) AudioSystem.getLine(dataLineInfo); } + + private Optional inhibitorPath() { + Optional gnomeSessionInhibitorPathSpec = gnomeSessionInhibitPathSpec.get(); + return gnomeSessionInhibitorPathSpec.isPresent() ? gnomeSessionInhibitorPathSpec : systemdInhibitPathSpec.get(); + } + + private Optional inhibitCommand() { + if (inhibitorPathSpec.isPresent()) { + String cmd = inhibitorPathSpec.get(); + final String[] params; + if (cmd.contains("gnome-session-inhibit")) { + params = new String[]{cmd, "--app-id", "Bisq", "--inhibit", "suspend", "--reason", "Avoid Standby", "--inhibit-only"}; + } else { + // systemd-inhibit arguments: must run a command; there is no --inhibit-only option + params = new String[]{cmd, "--who", "Bisq", "--what", "sleep", "--why", "Avoid Standby", "--mode", "block", "tail", "-f", "/dev/null"}; + } + return Optional.of(params); + } else { + return Optional.empty(); + } + } + + private final Predicate isCommandInstalled = (p) -> { + File executable = Paths.get(p).toFile(); + return executable.exists() && executable.canExecute(); + }; + + private final Function> commandPath = (possiblePaths) -> { + for (String path : possiblePaths) { + if (isCommandInstalled.test(path)) { + return Optional.of(path); + } + } + return Optional.empty(); + }; + + private final Supplier> gnomeSessionInhibitPathSpec = () -> + commandPath.apply(new String[]{"/usr/bin/gnome-session-inhibit", "/bin/gnome-session-inhibit"}); + + private final Supplier> systemdInhibitPathSpec = () -> + commandPath.apply(new String[]{"/usr/bin/systemd-inhibit", "/bin/systemd-inhibit"}); }