// Copyright (C) 2026, 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.Collections.Generic;
using System.Threading.Tasks;
using CoCoL;
using Duplicati.Library.Interface;
using Duplicati.Library.Main.IPC.Dto;
using Duplicati.Library.Utility;
#nullable enable
namespace Duplicati.Library.Main.IPC;
///
/// Server-side implementation of the controller RPC interface.
/// This will be spawned in a separate process and call back to the server process.
///
public class ControllerRpcServer : IController, IDisposable
{
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType();
private Controller? _controller;
private readonly IControllerRpcCallbacks _callbacks;
private CallbackMessageSink? _callbackSink;
///
/// Gets or sets the callback for when an operation is started
///
public Action? OnOperationStarted { get; set; }
///
/// Gets or sets the callback for when an operation is completed
///
public Action? OnOperationCompleted { get; set; }
///
/// Creates a new controller RPC server
///
public ControllerRpcServer(IControllerRpcCallbacks callbacks)
{
_callbacks = callbacks;
}
///
/// Initializes the controller with the given backend URL and options
///
public Task InitializeAsync(string backendUrl, Dictionary options)
{
if (_controller != null)
throw new InvalidOperationException("Controller is already initialized");
_callbackSink = new CallbackMessageSink(_callbacks);
_controller = new Controller(backendUrl, options, _callbackSink)
{
// Wire up events
OnOperationStarted = (results) =>
{
// Try to get the operation mode from the results
OperationMode operation = OperationMode.Backup;
if (results is IBackupResults)
operation = OperationMode.Backup;
else if (results is IRestoreResults)
operation = OperationMode.Restore;
else if (results is IListResults)
operation = OperationMode.List;
else if (results is IDeleteResults)
operation = OperationMode.Delete;
else if (results is IRepairResults)
operation = OperationMode.Repair;
else if (results is ICompactResults)
operation = OperationMode.Compact;
else if (results is IVacuumResults)
operation = OperationMode.Vacuum;
else if (results is ITestResults)
operation = OperationMode.Test;
else if (results is ICreateLogDatabaseResults)
operation = OperationMode.CreateLogDb;
else if (results is IListRemoteResults)
operation = OperationMode.ListRemote;
else if (results is ISetLockResults)
operation = OperationMode.SetLock;
else if (results is IReadLockInfoResults)
operation = OperationMode.ReadLockInfo;
_callbacks?.OnOperationStartedAsync(operation).FireAndForget();
OnOperationStarted?.Invoke(results);
},
OnOperationCompleted = (results, exception) =>
{
var resultDto = OperationResultDto.FromResults(results);
var exceptionDto = exception != null ? ExceptionDto.FromException(exception) : null;
_callbacks?.OnOperationCompletedAsync(resultDto, exceptionDto).FireAndForget();
OnOperationCompleted?.Invoke(results, exception);
}
};
return Task.CompletedTask;
}
private Controller Controller => _controller ?? throw new InvalidOperationException("Controller is not initialized");
#region IController Implementation
///
public Task BackupAsync(string[] inputsources, IFilter? filter = null)
=> Controller.BackupAsync(inputsources, filter);
///
public Task RestoreAsync(string[]? paths, IFilter? filter = null)
=> Controller.RestoreAsync(paths, filter);
///
public Task ListAsync()
=> Controller.ListAsync();
///
public Task ListAsync(string? filterstring)
=> Controller.ListAsync(filterstring);
///
public Task ListAsync(IEnumerable? filterstrings, IFilter? filter)
=> Controller.ListAsync(filterstrings, filter);
///
public Task DeleteAsync()
=> Controller.DeleteAsync();
///
public Task RepairAsync(IFilter? filter = null)
=> Controller.RepairAsync(filter);
///
public Task CompactAsync()
=> Controller.CompactAsync();
///
public Task VacuumAsync()
=> Controller.VacuumAsync();
///
public Task TestAsync(long samples = 1)
=> Controller.TestAsync(samples);
///
public Task ListFilesetsAsync()
=> Controller.ListFilesetsAsync();
///
public Task ListFolderAsync(string[]? folders, long offset, long limit, bool extendedData)
=> Controller.ListFolderAsync(folders, offset, limit, extendedData);
///
public Task ListFileVersionsAsync(string[]? files, long offset, long limit)
=> Controller.ListFileVersionsAsync(files, offset, limit);
///
public Task SearchEntriesAsync(string[]? pathprefixes, IFilter? filter, bool caseSensitive, long offset, long limit, bool extendedData)
=> Controller.SearchEntriesAsync(pathprefixes, filter, caseSensitive, offset, limit, extendedData);
///
public Task CreateLogDatabaseAsync(string targetpath)
=> Controller.CreateLogDatabaseAsync(targetpath);
///
public Task ListRemoteAsync()
=> Controller.ListRemoteAsync();
///
public Task DeleteAllRemoteFilesAsync()
=> Controller.DeleteAllRemoteFilesAsync();
///
public Task SetLocksAsync()
=> Controller.SetLocksAsync();
///
public Task ReadLockInfoAsync()
=> Controller.ReadLockInfoAsync();
///
public Task UpdateDatabaseWithVersionsAsync(IFilter? filter = null)
=> Controller.UpdateDatabaseWithVersionsAsync(filter);
///
public Task PurgeFilesAsync(IFilter? filter)
=> Controller.PurgeFilesAsync(filter);
///
public Task ListBrokenFilesAsync(IFilter? filter, Func? callbackhandler = null)
=> Controller.ListBrokenFilesAsync(filter, null);
///
public Task PurgeBrokenFilesAsync(IFilter? filter)
=> Controller.PurgeBrokenFilesAsync(filter);
///
public Task SendMailAsync()
=> Controller.SendMailAsync();
///
public Task SystemInfoAsync()
=> Controller.SystemInfoAsync();
///
public Task TestFilterAsync(string[] paths, IFilter? filter = null)
=> Controller.TestFilterAsync(paths, filter);
///
public Task ListChangesAsync(string baseVersion, string targetVersion, IEnumerable? filterstrings = null, IFilter? filter = null, Action>>? callback = null)
=> Controller.ListChangesAsync(baseVersion, targetVersion, filterstrings, filter, null);
///
public Task ListAffectedAsync(List? args, Action? callback = null)
=> Controller.ListAffectedAsync(args, null);
///
public Task RestoreControlFilesAsync(IEnumerable? files = null, IFilter? filter = null)
=> Controller.RestoreControlFilesAsync(files, filter);
///
public Task ListControlFilesAsync(IEnumerable? filterstrings, IFilter? filter)
=> Controller.ListControlFilesAsync(filterstrings, filter);
///
public async Task StopAsync()
{
if (_controller != null)
await _controller.StopAsync().ConfigureAwait(false);
}
///
public async Task AbortAsync()
{
if (_controller != null)
await _controller.AbortAsync().ConfigureAwait(false);
}
///
public async Task PauseAsync(bool alsoTransfers)
{
if (_controller != null)
await _controller.PauseAsync(alsoTransfers).ConfigureAwait(false);
}
///
public async Task ResumeAsync()
{
if (_controller != null)
await _controller.ResumeAsync().ConfigureAwait(false);
}
///
public async Task SetThrottleSpeedsAsync(long maxUploadPrSecond, long maxDownloadPrSecond)
{
if (_controller != null)
await _controller.SetThrottleSpeedsAsync(maxUploadPrSecond, maxDownloadPrSecond).ConfigureAwait(false);
}
///
public async Task SetSecretProviderAsync(ISecretProvider? secretProvider)
{
if (_controller != null)
await _controller.SetSecretProviderAsync(secretProvider).ConfigureAwait(false);
}
///
public async Task SetLastCompactAsync(DateTime value)
{
if (_controller != null)
await _controller.SetLastCompactAsync(value).ConfigureAwait(false);
}
///
public async Task SetLastVacuumAsync(DateTime value)
{
if (_controller != null)
await _controller.SetLastVacuumAsync(value).ConfigureAwait(false);
}
///
public Task AppendSinkAsync(IMessageSink sink)
{
// In out-of-process mode, additional sinks cannot be added dynamically
Library.Logging.Log.WriteWarningMessage(LOGTAG, "AppendSinkNotSupported", null, "AppendSink is not supported in out-of-process mode");
return Task.CompletedTask;
}
#endregion
#region Progress Queries
///
/// Gets the current backend progress
///
public Task GetBackendProgressAsync()
{
var progress = _callbackSink?.BackendProgress;
return Task.FromResult(BackendProgressDto.FromProgress(progress)!);
}
///
/// Gets the current operation progress
///
public Task GetOperationProgressAsync()
{
var progress = _callbackSink?.OperationProgress;
return Task.FromResult(OperationProgressDto.FromProgress(progress)!);
}
#endregion
#region IDisposable
///
public void Dispose()
{
_callbackSink?.Dispose();
_callbackSink = null;
_controller?.Dispose();
_controller = null;
}
#endregion
///
/// Disposes the server asynchronously
///
public Task DisposeAsync()
{
Dispose();
return Task.CompletedTask;
}
}
///
/// Message sink that forwards messages to the RPC callbacks
///
public class CallbackMessageSink : IMessageSink, IDisposable
{
private readonly IControllerRpcCallbacks _callbacks;
///
/// The current backend progress
///
public IBackendProgress? BackendProgress { get; private set; }
///
/// The current operation progress
///
public IOperationProgress? OperationProgress { get; private set; }
public CallbackMessageSink(IControllerRpcCallbacks callbacks)
{
_callbacks = callbacks;
}
///
public void WriteMessage(Logging.LogEntry entry)
{
if (_callbacks != null)
{
var dto = LogEntryDto.FromEntry(entry);
_callbacks.OnLogMessageAsync(dto).FireAndForget();
}
}
///
public void BackendEvent(BackendActionType action, BackendEventType type, string path, long size)
{
_callbacks?.OnBackendEventAsync(action, type, path, size).FireAndForget();
}
///
public void SetBackendProgress(IBackendProgress progress)
{
BackendProgress = progress;
_callbacks?.OnBackendProgressAsync(BackendProgressDto.FromProgress(progress)).FireAndForget();
}
///
public void SetOperationProgress(IOperationProgress progress)
{
if (OperationProgress != null)
OperationProgress.PhaseChanged -= OnPhaseChanged;
OperationProgress = progress;
if (OperationProgress != null)
{
OperationProgress.PhaseChanged += OnPhaseChanged;
_callbacks?.OnOperationProgressAsync(OperationProgressDto.FromProgress(progress)).FireAndForget();
}
}
private void OnPhaseChanged(OperationPhase phase, OperationPhase previousPhase)
{
_callbacks?.OnPhaseChangedAsync(phase, previousPhase).FireAndForget();
_callbacks?.OnOperationProgressAsync(OperationProgressDto.FromProgress(OperationProgress)).FireAndForget();
}
///
public void Dispose()
{
if (OperationProgress != null)
{
OperationProgress.PhaseChanged -= OnPhaseChanged;
OperationProgress = null;
}
}
}