// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Linq; using Duplicati.Library.Common; using Duplicati.Library.Utility; namespace Duplicati.Library.Main { /// /// This class provides various process control tasks, /// such as preventing sleep and setting the IO priority of /// the running process /// public class ProcessController : IDisposable { /// /// The log tag to use /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(); /// /// A flag used to control the stop invocation /// private bool m_disposed = true; /// /// A flag indicating if the sleep prevention has been started /// private bool m_runningSleepPrevention; /// /// A flag indicating if the background IO priority has been started /// private bool m_hasEnabledBackgroundIOPriority; /// /// The caffeinate process runner /// private System.Diagnostics.Process m_caffeinate; /// /// The nice level to restore the process to /// private int m_originalNiceLevel; /// /// The nice class to restore the process to /// private int m_originalNiceClass; /// /// The priority class to restore the process to /// private Win32.IO_PRIORITY_HINT m_originalWinPriorityClass; /// /// A flag indicating if the Windows background mode is started /// private bool m_hasStartedBackgroundMode = false; /// /// Initializes a new instance of the class. /// /// The options to use. public ProcessController(Options options) { if (options == null) return; try { Start(options); m_disposed = false; } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "ProcessControllerStartError", ex, "Failed to start the process controller: {0}", ex.Message); } } /// /// Starts the sleep prevention /// private void StartSleepPrevention() { if (OperatingSystem.IsWindows()) { try { Win32.SetThreadExecutionState(Win32.EXECUTION_STATE.ES_CONTINUOUS | Win32.EXECUTION_STATE.ES_SYSTEM_REQUIRED); m_runningSleepPrevention = true; } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "SleepPrevetionError", ex, "Failed to set sleep prevention"); } } else if (OperatingSystem.IsMacOS()) { try { // -s prevents sleep on AC, -i prevents sleep generally var psi = new System.Diagnostics.ProcessStartInfo("caffeinate", "-s") { RedirectStandardInput = true, RedirectStandardError = false, RedirectStandardOutput = false, UseShellExecute = false }; m_caffeinate = System.Diagnostics.Process.Start(psi); m_runningSleepPrevention = true; } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "SleepPreventionError", ex, "Failed to set sleep prevention"); } } else { } } /// /// Activates the process background IO priority /// private void ActivateBackgroundIOPriority() { var pid = System.Diagnostics.Process.GetCurrentProcess().Id; if (OperatingSystem.IsWindows()) { var handle = System.Diagnostics.Process.GetCurrentProcess().Handle; try { var mode = Win32.IO_PRIORITY_HINT.IoPriorityLow; var res = Win32.NtQueryInformationProcess(handle, Win32.PROCESS_INFORMATION_CLASS.ProcessIoPriority, ref mode, sizeof(Win32.IO_PRIORITY_HINT), IntPtr.Zero); if (res != 0) throw new Library.Interface.UserInformationException($"Failed to read process priority {res:x}", "BackgroundPriorityEnableError", new System.ComponentModel.Win32Exception()); m_originalWinPriorityClass = mode; mode = Win32.IO_PRIORITY_HINT.IoPriorityVeryLow; res = Win32.NtSetInformationProcess(handle, Win32.PROCESS_INFORMATION_CLASS.ProcessIoPriority, ref mode, sizeof(Win32.IO_PRIORITY_HINT)); if (res != 0) throw new Library.Interface.UserInformationException($"Failed to set process priority {res:x}", "BackgroundPriorityEnableError", new System.ComponentModel.Win32Exception()); m_hasEnabledBackgroundIOPriority = true; } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "BackgroundPriorityError", ex, "Failed to set background IO priority"); } try { if (!Win32.SetPriorityClass(handle, Win32.PROCESS_PRIORITY_CLASS.PROCESS_MODE_BACKGROUND_BEGIN)) throw new Library.Interface.UserInformationException($"Failed to start process background mode", "BackgroundPriorityEnableError", new System.ComponentModel.Win32Exception()); m_hasStartedBackgroundMode = true; } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "BackgroundPriorityError", ex, "Failed to set start background processing mode"); } } else { if (OperatingSystem.IsMacOS()) { var data = RunProcessAndGetResult("ps", $"-onice -p {pid}"); if (data.Item1 != 0) { Logging.Log.WriteWarningMessage(LOGTAG, "BackgroundPriorityError", null, "Failed to get background IO priority, exitcode: {0}, stderr: {1}", data.Item1, data.Item3); } else { m_originalNiceLevel = int.Parse(data.Item2.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).Last()); data = RunProcessAndGetResult("renice", $"20 -p {pid}"); if (data.Item1 != 0) Logging.Log.WriteWarningMessage(LOGTAG, "BackgroundPriorityError", null, "Failed to get background IO priority, exitcode: {0}, stderr: {1}", data.Item1, data.Item3); else m_hasEnabledBackgroundIOPriority = true; } } else { var data = RunProcessAndGetResult("ionice", $"-p {pid}"); var results = data.Item2.Split(new char[] { ':', ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); var ioclass = results[0]; if (string.Equals(ioclass, "idle", StringComparison.OrdinalIgnoreCase)) { m_originalNiceClass = 3; // Only allowed for "best-effort" and "realtime" m_originalNiceLevel = -1; } else if (string.Equals(ioclass, "none", StringComparison.OrdinalIgnoreCase)) { m_originalNiceClass = 0; // Only allowed for "best-effort" and "realtime" m_originalNiceLevel = -1; } else if (string.Equals(ioclass, "best-effort", StringComparison.OrdinalIgnoreCase)) { m_originalNiceClass = 2; m_originalNiceLevel = int.Parse(results.Last()); } else if (string.Equals(ioclass, "realtime", StringComparison.OrdinalIgnoreCase)) { m_originalNiceClass = 1; m_originalNiceLevel = int.Parse(results.Last()); } else throw new Library.Interface.UserInformationException($"Unable to parse priority class {ioclass}", "UnableToParseIONicePriorityClass"); data = RunProcessAndGetResult("ionice", $"-c 3 -p {pid}"); m_hasEnabledBackgroundIOPriority = true; } } } /// /// Expose all filesystem attributes /// private static void ExposeAllFilesystemAttributes() { // Starting with Windows 10 1803, the operating system may mask the process's view of some // file attributes such as reparse, offline, and sparse. // // This function will turn off such masking. // // See https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-rtlqueryprocessplaceholdercompatibilitymode if (OperatingSystem.IsWindows()) { try { Win32.RtlSetProcessPlaceholderCompatibilityMode(Win32.PHCM_VALUES.PHCM_EXPOSE_PLACEHOLDERS); } catch { // Ignore exceptions - not applicable on this version of Windows } } } /// /// Starts the process controller /// /// The options to use private void Start(Options options) { if (!options.AllowSleep) StartSleepPrevention(); if (options.UseBackgroundIOPriority) ActivateBackgroundIOPriority(); ExposeAllFilesystemAttributes(); } /// /// Stops the sleep prevention, if it was enabled /// private void StopSleepPrevention() { if (OperatingSystem.IsWindows()) { try { if (m_runningSleepPrevention) { m_runningSleepPrevention = false; Win32.SetThreadExecutionState(Win32.EXECUTION_STATE.ES_CONTINUOUS); } } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "SleepPrevetionError", ex, "Failed to set sleep prevention"); } } else if (OperatingSystem.IsMacOS()) { try { m_runningSleepPrevention = false; if (m_caffeinate != null && !m_caffeinate.HasExited) { // Send CTRL+C m_caffeinate.StandardInput.Write("\x3"); m_caffeinate.StandardInput.Flush(); m_caffeinate.WaitForExit(500); if (!m_caffeinate.HasExited) { m_caffeinate.Kill(); m_caffeinate.WaitForExit(500); if (!m_caffeinate.HasExited) throw new Exception("Failed to kill the caffeinate process"); } } } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "SleepPreventionDisableError", ex, "Failed to unset sleep prevention"); } } } /// /// Deactivates the background IO Priority, if set. /// private void DeactivateBackgroundIOPriority() { if (OperatingSystem.IsWindows()) { try { if (m_hasStartedBackgroundMode) { m_hasStartedBackgroundMode = false; var handle = System.Diagnostics.Process.GetCurrentProcess().Handle; if (!Win32.SetPriorityClass(handle, Win32.PROCESS_PRIORITY_CLASS.PROCESS_MODE_BACKGROUND_END)) throw new Library.Interface.UserInformationException($"Failed to stop process background mode", "BackgroundPriorityEnableError", new System.ComponentModel.Win32Exception()); } } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "BackgroundPriorityError", ex, "Failed to stop start background processing mode"); } try { if (m_hasEnabledBackgroundIOPriority) { m_hasEnabledBackgroundIOPriority = false; var handle = System.Diagnostics.Process.GetCurrentProcess().Handle; var mode = m_originalWinPriorityClass; var res = Win32.NtSetInformationProcess(handle, Win32.PROCESS_INFORMATION_CLASS.ProcessIoPriority, ref mode, sizeof(Win32.IO_PRIORITY_HINT)); if (res != 0) Logging.Log.WriteWarningMessage(LOGTAG, "BackgroundPriorityDisableError", new System.ComponentModel.Win32Exception(), "Failed to reset background IO priority, status code {0}", res); } } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "BackgroundPriorityError", ex, "Failed to reset background IO priority"); } } else { if (m_hasEnabledBackgroundIOPriority) { m_hasEnabledBackgroundIOPriority = false; var pid = System.Diagnostics.Process.GetCurrentProcess().Id; Tuple data; if (OperatingSystem.IsMacOS()) { // TODO: We can only give lower priority, thus not reset it ... data = RunProcessAndGetResult($"renice", $"{m_originalNiceLevel} -p {pid}"); if (data.Item1 != 0) Logging.Log.WriteWarningMessage(LOGTAG, "BackgroundPriorityError", null, "Failed to reset background IO priority, exitcode: {0}, stderr: {1}", data.Item1, data.Item3); } else { if (m_originalNiceLevel < 0) data = RunProcessAndGetResult($"ionice", $"-c {m_originalNiceClass} -p {pid}"); else data = RunProcessAndGetResult($"ionice", $"-c {m_originalNiceClass} -n {m_originalNiceLevel} -p {pid}"); if (!string.IsNullOrWhiteSpace(data.Item3)) Logging.Log.WriteWarningMessage(LOGTAG, "BackgroundPriorityError", null, "Failed to reset background IO priority, exitcode: {0}, stderr: {1}", data.Item1, data.Item3); } } } } /// /// Stops the process controller /// private void Stop() { StopSleepPrevention(); DeactivateBackgroundIOPriority(); } /// /// Runs a process and returns the stdout data /// /// The stdout data. /// The executable to invoke. /// The commandline arguments. private static Tuple RunProcessAndGetResult(string filename, string arguments) { var psi = new System.Diagnostics.ProcessStartInfo(filename, arguments) { RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = false, UseShellExecute = false }; Logging.Log.WriteExplicitMessage(LOGTAG, "RunningCommand", null, "Running: {0} {1}", filename, arguments); var pi = System.Diagnostics.Process.Start(psi); pi.WaitForExit(5000); if (pi.HasExited) { return new Tuple( pi.ExitCode, pi.StandardOutput.ReadToEnd().Trim(), pi.StandardError.ReadToEnd().Trim() ); } pi.Kill(); throw new Library.Interface.UserInformationException($"The process {filename} with arguments {arguments} failed to stop", "LaunchProcessFailed"); } /// /// Releases all resource used by the object. /// /// Call when you are finished using the /// . The method leaves the /// in an unusable state. After calling /// , you must release all references to the /// so the garbage collector can reclaim the memory /// that the was occupying. public void Dispose() { if (!m_disposed) { m_disposed = true; try { Stop(); } catch(Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "ProcessControllerStopError", ex, "Failed to stop the process controller: {0}", ex.Message); } } } } }