// 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; } } }