2026-04-16 15:21:24 +02:00
// Copyright (C) 2026, The Duplicati Team
2025-01-07 09:40:39 +01:00
// 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.
2024-06-07 15:56:43 +02:00
using System.Text ;
2025-06-25 10:06:36 +02:00
using Duplicati.Library.Utility ;
2024-06-07 15:56:43 +02:00
using Duplicati.Server ;
using Duplicati.WebserverCore.Abstractions ;
namespace Duplicati.WebserverCore.Services ;
2025-05-05 13:41:28 +02:00
public class CommandlineRunService ( IQueueRunnerService queueRunnerService , ILogWriteHandler logWriteHandler ) : ICommandlineRunService
2024-06-07 15:56:43 +02:00
{
private static readonly string LOGTAG = Library . Logging . Log . LogTagFromType < CommandlineRunService >();
2025-05-05 13:41:28 +02:00
private class LogWriter ( ActiveRun target , ILogWriteHandler logWriteHandler ) : TextWriter
2024-06-07 15:56:43 +02:00
{
2025-05-05 13:41:28 +02:00
private readonly StringBuilder sb = new StringBuilder ();
2024-06-07 15:56:43 +02:00
private int m_newlinechars = 0 ;
public override Encoding Encoding => Encoding . UTF8 ;
public override void Write ( char value )
{
2025-05-05 13:41:28 +02:00
lock ( target . Lock )
2024-06-07 15:56:43 +02:00
{
2025-05-05 13:41:28 +02:00
sb . Append ( value );
2024-06-07 15:56:43 +02:00
if ( NewLine [ m_newlinechars ] == value )
{
m_newlinechars ++;
if ( m_newlinechars == NewLine . Length )
WriteLine ( string . Empty );
}
else
m_newlinechars = 0 ;
}
}
public override void WriteLine ( string? value )
{
value ??= string . Empty ;
2025-05-05 13:41:28 +02:00
lock ( target . Lock )
2024-06-07 15:56:43 +02:00
{
2025-05-05 13:41:28 +02:00
target . LastAccess = DateTime . Now ;
2024-06-07 15:56:43 +02:00
//Avoid writing the log if it does not exist
2025-05-05 13:41:28 +02:00
if ( target . IsLogDisposed )
2024-06-07 15:56:43 +02:00
{
2025-05-05 13:41:28 +02:00
logWriteHandler . WriteMessage ( new Library . Logging . LogEntry ( "Attempted to write message after closing: {0}" , new object [] { value }, Library . Logging . LogMessageType . Warning , LOGTAG , "CommandLineOutputAfterLogClosed" , null ));
2024-06-07 15:56:43 +02:00
return ;
}
try
{
2025-05-05 13:41:28 +02:00
if ( sb . Length != 0 )
2024-06-07 15:56:43 +02:00
{
2025-05-05 13:41:28 +02:00
target . Log . Add ( sb + value );
sb . Length = 0 ;
2024-06-07 15:56:43 +02:00
m_newlinechars = 0 ;
}
else
{
2025-05-05 13:41:28 +02:00
target . Log . Add ( value );
2024-06-07 15:56:43 +02:00
}
}
catch ( Exception ex )
{
// This can happen on a very unlucky race where IsLogDisposed is set right after the check
2025-05-05 13:41:28 +02:00
logWriteHandler . WriteMessage ( new Library . Logging . LogEntry ( "Failed to forward commandline message: {0}" , new object [] { value }, Library . Logging . LogMessageType . Warning , LOGTAG , "CommandLineOutputAfterLogClosed" , ex ));
2024-06-07 15:56:43 +02:00
}
}
}
}
private class ActiveRun : ICommandlineRunService . IActiveRun
{
public string ID { get ; } = Guid . NewGuid (). ToString ();
public DateTime LastAccess { get ; set ; } = DateTime . Now ;
public readonly Library . Utility . FileBackedStringList Log = new Library . Utility . FileBackedStringList ();
public Runner . IRunnerData ? Task ;
public LogWriter ? Writer ;
public object Lock { get ; } = new object ();
public bool Finished { get ; set ; } = false ;
public bool Started { get ; set ; } = false ;
public bool IsLogDisposed { get ; set ; } = false ;
public IEnumerable < string > GetLog () => Log ;
public void Abort ()
{
var tt = this . Task ;
if ( tt != null )
tt . Abort ();
}
}
private readonly Dictionary < string , ActiveRun > m_activeItems = new Dictionary < string , ActiveRun >();
private Task ? m_cleanupTask ;
public ICommandlineRunService . IActiveRun ? GetActiveRun ( string id )
{
ActiveRun ? t ;
lock ( m_activeItems )
m_activeItems . TryGetValue ( id , out t );
if ( t != null )
lock ( t . Lock )
t . LastAccess = DateTime . Now ;
return t ;
}
public string StartTask ( string [] args )
{
var k = new ActiveRun ();
2025-05-05 13:41:28 +02:00
k . Writer = new LogWriter ( k , logWriteHandler );
2024-06-07 15:56:43 +02:00
m_activeItems [ k . ID ] = k ;
StartCleanupTask ();
k . Task = Runner . CreateCustomTask (( sink ) =>
{
try
{
k . Started = true ;
2025-06-25 10:06:36 +02:00
// Expand sources for supporting %DOCUMENTS% and similar
if ( args . Length > 2 && args [ 0 ]. Equals ( "backup" , StringComparison . OrdinalIgnoreCase ))
{
var options = args . Skip ( 1 ). Where ( x => x . StartsWith ( "--" , StringComparison . OrdinalIgnoreCase ))
. ToArray ();
var allargs =
args . Skip ( 1 )
. Where ( x => ! x . StartsWith ( "--" , StringComparison . OrdinalIgnoreCase ));
if ( allargs . Count () > 1 )
{
var sources = allargs
. Skip ( 1 )
. Select ( SpecialFolders . ExpandEnvironmentVariables )
. WhereNotNullOrWhiteSpace ()
. ToArray ();
args = [ args [ 0 ], allargs . First (), .. sources , .. options ];
}
}
2024-06-07 15:56:43 +02:00
var code = CommandLine . Program . RunCommandLine ( k . Writer , k . Writer , c =>
{
k . Task !. SetController ( c );
c . AppendSink ( sink );
}, args );
k . Writer . WriteLine ( "Return code: {0}" , code );
}
catch ( Exception ex )
{
var rx = ex ;
if ( rx is System . Reflection . TargetInvocationException )
rx = rx . InnerException ;
if ( rx is Library . Interface . UserInformationException )
k . Log . Add ( rx . Message );
else
k . Log . Add ( rx ?. ToString () ?? "null exception?" );
throw rx ?? new Exception ( "null exception?" );
}
finally
{
k . Finished = true ;
}
});
2025-05-03 14:58:07 +02:00
queueRunnerService . AddTask ( k . Task );
2024-06-07 15:56:43 +02:00
return k . ID ;
}
private void StartCleanupTask ()
{
if ( m_cleanupTask == null || m_cleanupTask . IsCompleted || m_cleanupTask . IsFaulted || m_cleanupTask . IsCanceled )
m_cleanupTask = RunCleanupAsync ();
}
private async Task RunCleanupAsync ()
{
while ( m_activeItems . Count > 0 )
{
var oldest = m_activeItems . Values
. OrderBy ( x => x . LastAccess )
. FirstOrDefault ();
if ( oldest != null )
{
// If the task has finished, we just wait a little to allow the UI to pick it up
var timeout = oldest . Finished ? TimeSpan . FromMinutes ( 5 ) : TimeSpan . FromDays ( 1 );
if ( DateTime . Now - oldest . LastAccess > timeout )
{
oldest . IsLogDisposed = true ;
m_activeItems . Remove ( oldest . ID );
oldest . Log . Dispose ();
// Fix all expired, or stop running
continue ;
}
}
await Task . Delay ( TimeSpan . FromMinutes ( 1 )). ConfigureAwait ( false );
}
}
}