2024-02-28 15:45:30 +01:00
// 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.
2019-11-30 11:35:43 -08:00
using System ;
using System.Linq ;
using Duplicati.Library.Common ;
using Duplicati.Library.Utility ;
namespace Duplicati.Library.Main
2018-03-16 10:33:26 +01:00
{
/// <summary>
/// This class provides various process control tasks,
/// such as preventing sleep and setting the IO priority of
/// the running process
2019-11-30 11:35:43 -08:00
/// </summary>
public class ProcessController : IDisposable
2018-03-16 10:33:26 +01:00
{
/// <summary>
/// The log tag to use
/// </summary>
private static readonly string LOGTAG = Logging . Log . LogTagFromType < ProcessController >();
/// <summary>
/// A flag used to control the stop invocation
/// </summary>
private bool m_disposed = true ;
/// <summary>
/// A flag indicating if the sleep prevention has been started
/// </summary>
private bool m_runningSleepPrevention ;
2018-03-16 23:25:06 +01:00
/// <summary>
/// A flag indicating if the background IO priority has been started
/// </summary>
private bool m_hasEnabledBackgroundIOPriority ;
2018-03-16 10:33:26 +01:00
/// <summary>
/// The caffeinate process runner
/// </summary>
private System . Diagnostics . Process m_caffeinate ;
2018-03-16 23:25:06 +01:00
/// <summary>
/// The nice level to restore the process to
/// </summary>
private int m_originalNiceLevel ;
/// <summary>
/// The nice class to restore the process to
/// </summary>
private int m_originalNiceClass ;
/// <summary>
/// The priority class to restore the process to
/// </summary>
private Win32 . IO_PRIORITY_HINT m_originalWinPriorityClass ;
/// <summary>
/// A flag indicating if the Windows background mode is started
/// </summary>
private bool m_hasStartedBackgroundMode = false ;
2018-03-16 10:33:26 +01:00
/// <summary>
/// Initializes a new instance of the <see cref="T:Duplicati.Library.Main.ProcessController"/> class.
/// </summary>
2019-11-30 11:35:43 -08:00
/// <param name="options">The options to use.</param>
public ProcessController ( Options options )
2018-03-16 10:33:26 +01:00
{
2018-03-16 23:25:06 +01:00
if ( options == null )
2018-03-16 10:33:26 +01:00
return ;
try
{
2018-03-16 23:25:06 +01:00
Start ( options );
2018-03-16 10:33:26 +01:00
m_disposed = false ;
}
catch ( Exception ex )
{
Logging . Log . WriteWarningMessage ( LOGTAG , "ProcessControllerStartError" , ex , "Failed to start the process controller: {0}" , ex . Message );
}
2019-11-30 11:35:43 -08:00
2018-03-16 10:33:26 +01:00
}
/// <summary>
2018-03-16 23:25:06 +01:00
/// Starts the sleep prevention
2018-03-16 10:33:26 +01:00
/// </summary>
2018-03-16 23:25:06 +01:00
private void StartSleepPrevention ()
2018-03-16 10:33:26 +01:00
{
2018-11-02 22:13:25 +01:00
if ( Platform . IsClientWindows )
2018-03-16 10:33:26 +01:00
{
2018-03-16 23:25:06 +01:00
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" );
}
}
2018-11-02 22:13:25 +01:00
else if ( Platform . IsClientOSX )
2018-03-16 23:25:06 +01:00
{
try
{
// -s prevents sleep on AC, -i prevents sleep generally
2019-11-30 11:35:43 -08:00
var psi = new System . Diagnostics . ProcessStartInfo ( "caffeinate" , "-s" )
{
2018-06-12 09:33:09 +02:00
RedirectStandardInput = true ,
RedirectStandardError = false ,
2019-11-30 11:35:43 -08:00
RedirectStandardOutput = false ,
UseShellExecute = false
2018-06-12 09:33:09 +02:00
};
2018-03-16 23:25:06 +01:00
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
{
}
}
/// <summary>
2019-11-30 11:35:43 -08:00
/// Activates the process background IO priority
2018-03-16 23:25:06 +01:00
/// </summary>
private void ActivateBackgroundIOPriority ()
{
var pid = System . Diagnostics . Process . GetCurrentProcess (). Id ;
2018-11-02 22:13:25 +01:00
if ( Platform . IsClientWindows )
2018-03-16 23:25:06 +01:00
{
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
{
2018-11-02 22:13:25 +01:00
if ( Platform . IsClientOSX )
2018-03-16 23:25:06 +01:00
{
var data = RunProcessAndGetResult ( "ps" , $"-onice -p {pid}" );
2018-04-09 14:26:53 +02:00
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 ());
2018-03-16 23:25:06 +01:00
2018-04-09 14:26:53 +02:00
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 ;
}
2018-03-16 23:25:06 +01:00
}
else
2018-03-16 10:33:26 +01:00
{
2018-03-16 23:25:06 +01:00
var data = RunProcessAndGetResult ( "ionice" , $"-p {pid}" );
2018-04-09 14:26:53 +02:00
var results = data . Item2 . Split ( new char [] { ':' , ' ' , '\t' , '\r' , '\n' }, StringSplitOptions . RemoveEmptyEntries );
2018-03-16 23:25:06 +01:00
var ioclass = results [ 0 ];
if ( string . Equals ( ioclass , "idle" , StringComparison . OrdinalIgnoreCase ))
2018-03-16 10:33:26 +01:00
{
2018-03-16 23:25:06 +01:00
m_originalNiceClass = 3 ;
2020-11-05 15:49:48 +01:00
// Only allowed for "best-effort" and "realtime"
2018-03-16 23:25:06 +01:00
m_originalNiceLevel = - 1 ;
2018-03-16 10:33:26 +01:00
}
2018-03-16 23:25:06 +01:00
else if ( string . Equals ( ioclass , "none" , StringComparison . OrdinalIgnoreCase ))
2018-03-16 10:33:26 +01:00
{
2018-03-16 23:25:06 +01:00
m_originalNiceClass = 0 ;
2020-11-05 15:49:48 +01:00
// Only allowed for "best-effort" and "realtime"
m_originalNiceLevel = - 1 ;
2018-03-16 10:33:26 +01:00
}
2018-03-16 23:25:06 +01:00
else if ( string . Equals ( ioclass , "best-effort" , StringComparison . OrdinalIgnoreCase ))
2018-03-16 10:33:26 +01:00
{
2018-03-16 23:25:06 +01:00
m_originalNiceClass = 2 ;
m_originalNiceLevel = int . Parse ( results . Last ());
2018-03-16 10:33:26 +01:00
}
2018-03-16 23:25:06 +01:00
else if ( string . Equals ( ioclass , "realtime" , StringComparison . OrdinalIgnoreCase ))
2018-03-16 10:33:26 +01:00
{
2018-03-16 23:25:06 +01:00
m_originalNiceClass = 1 ;
m_originalNiceLevel = int . Parse ( results . Last ());
2018-03-16 10:33:26 +01:00
}
2018-03-16 23:25:06 +01:00
else
throw new Library . Interface . UserInformationException ( $"Unable to parse priority class {ioclass}" , "UnableToParseIONicePriorityClass" );
2018-03-16 10:33:26 +01:00
2018-03-16 23:25:06 +01:00
data = RunProcessAndGetResult ( "ionice" , $"-c 3 -p {pid}" );
m_hasEnabledBackgroundIOPriority = true ;
2018-03-16 10:33:26 +01:00
}
}
}
2019-11-22 08:00:58 -08:00
/// <summary>
/// Expose all filesystem attributes
/// </summary>
2019-11-22 08:26:38 -08:00
private static void ExposeAllFilesystemAttributes ()
2019-11-22 08:00:58 -08:00
{
// 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 ( Platform . IsClientWindows )
{
try
{
2019-11-26 19:27:46 -08:00
Win32 . RtlSetProcessPlaceholderCompatibilityMode ( Win32 . PHCM_VALUES . PHCM_EXPOSE_PLACEHOLDERS );
2019-11-22 08:00:58 -08:00
}
catch
{
2019-11-26 19:27:46 -08:00
// Ignore exceptions - not applicable on this version of Windows
2019-11-22 08:00:58 -08:00
}
}
}
2018-03-16 10:33:26 +01:00
/// <summary>
2018-03-16 23:25:06 +01:00
/// Starts the process controller
2018-03-16 10:33:26 +01:00
/// </summary>
2018-03-16 23:25:06 +01:00
/// <param name="options">The options to use</param>
private void Start ( Options options )
{
if (! options . AllowSleep )
StartSleepPrevention ();
if ( options . UseBackgroundIOPriority )
ActivateBackgroundIOPriority ();
2019-11-22 08:00:58 -08:00
ExposeAllFilesystemAttributes ();
2018-03-16 23:25:06 +01:00
}
/// <summary>
/// Stops the sleep prevention, if it was enabled
/// </summary>
private void StopSleepPrevention ()
2018-03-16 10:33:26 +01:00
{
2018-11-02 22:13:25 +01:00
if ( Platform . IsClientWindows )
2018-03-16 10:33:26 +01:00
{
try
{
if ( m_runningSleepPrevention )
2018-03-16 23:25:06 +01:00
{
m_runningSleepPrevention = false ;
2018-03-16 10:33:26 +01:00
Win32 . SetThreadExecutionState ( Win32 . EXECUTION_STATE . ES_CONTINUOUS );
2018-03-16 23:25:06 +01:00
}
2018-03-16 10:33:26 +01:00
}
catch ( Exception ex )
{
Logging . Log . WriteWarningMessage ( LOGTAG , "SleepPrevetionError" , ex , "Failed to set sleep prevention" );
}
}
2018-11-02 22:13:25 +01:00
else if ( Platform . IsClientOSX )
2018-03-16 10:33:26 +01:00
{
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" );
}
}
2018-03-16 23:25:06 +01:00
}
/// <summary>
/// Deactivates the background IO Priority, if set.
/// </summary>
private void DeactivateBackgroundIOPriority ()
{
2018-11-02 22:13:25 +01:00
if ( Platform . IsClientWindows )
2018-03-16 23:25:06 +01:00
{
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 ;
2018-04-09 14:26:53 +02:00
Tuple < int , string , string > data ;
2018-11-02 22:13:25 +01:00
if ( Platform . IsClientOSX )
2018-03-16 23:25:06 +01:00
{
2018-04-09 14:26:53 +02:00
// 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 );
2018-03-16 23:25:06 +01:00
}
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}" );
2018-04-09 14:26:53 +02:00
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 );
2018-03-16 23:25:06 +01:00
}
2018-04-09 14:26:53 +02:00
2018-03-16 23:25:06 +01:00
}
}
}
/// <summary>
/// Stops the process controller
/// </summary>
private void Stop ()
{
StopSleepPrevention ();
DeactivateBackgroundIOPriority ();
}
/// <summary>
/// Runs a process and returns the stdout data
/// </summary>
/// <returns>The stdout data.</returns>
/// <param name="filename">The executable to invoke.</param>
/// <param name="arguments">The commandline arguments.</param>
2018-04-09 14:26:53 +02:00
private static Tuple < int , string , string > RunProcessAndGetResult ( string filename , string arguments )
2018-03-16 23:25:06 +01:00
{
2019-11-30 11:35:43 -08:00
var psi = new System . Diagnostics . ProcessStartInfo ( filename , arguments )
{
RedirectStandardOutput = true ,
2018-06-12 09:33:09 +02:00
RedirectStandardError = true ,
2019-11-30 11:35:43 -08:00
RedirectStandardInput = false ,
UseShellExecute = false
2018-06-12 09:33:09 +02:00
};
2018-03-16 23:25:06 +01:00
Logging . Log . WriteExplicitMessage ( LOGTAG , "RunningCommand" , null , "Running: {0} {1}" , filename , arguments );
var pi = System . Diagnostics . Process . Start ( psi );
pi . WaitForExit ( 5000 );
if ( pi . HasExited )
2018-04-09 14:26:53 +02:00
{
return
new Tuple < int , string , string >(
pi . ExitCode ,
pi . StandardOutput . ReadToEnd (). Trim (),
pi . StandardError . ReadToEnd (). Trim ()
);
}
2018-03-16 23:25:06 +01:00
pi . Kill ();
throw new Library . Interface . UserInformationException ( $"The process {filename} with arguments {arguments} failed to stop" , "LaunchProcessFailed" );
2019-11-30 11:35:43 -08:00
}
2018-03-16 10:33:26 +01:00
/// <summary>
/// Releases all resource used by the <see cref="T:Duplicati.Library.Main.ProcessController"/> object.
/// </summary>
/// <remarks>Call <see cref="Dispose"/> when you are finished using the
/// <see cref="T:Duplicati.Library.Main.ProcessController"/>. The <see cref="Dispose"/> method leaves the
/// <see cref="T:Duplicati.Library.Main.ProcessController"/> in an unusable state. After calling
/// <see cref="Dispose"/>, you must release all references to the
/// <see cref="T:Duplicati.Library.Main.ProcessController"/> so the garbage collector can reclaim the memory
2019-11-30 11:35:43 -08:00
/// that the <see cref="T:Duplicati.Library.Main.ProcessController"/> was occupying.</remarks>
2018-03-16 10:33:26 +01:00
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 );
}
}
}
2019-11-30 11:35:43 -08:00
}
}