mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-31 17:11:52 +01:00
666 lines
20 KiB
C#
666 lines
20 KiB
C#
using Byond.TopicSender;
|
|
using Microsoft.Extensions.Logging;
|
|
using Newtonsoft.Json;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Tgstation.Server.Api.Models;
|
|
using Tgstation.Server.Host.Components.Byond;
|
|
using Tgstation.Server.Host.Components.Chat;
|
|
using Tgstation.Server.Host.Components.Deployment;
|
|
using Tgstation.Server.Host.Components.Interop;
|
|
using Tgstation.Server.Host.Components.Interop.Bridge;
|
|
using Tgstation.Server.Host.Components.Interop.Topic;
|
|
using Tgstation.Server.Host.System;
|
|
|
|
namespace Tgstation.Server.Host.Components.Session
|
|
{
|
|
/// <inheritdoc />
|
|
sealed class SessionController : ISessionController, IBridgeHandler, IChannelSink
|
|
{
|
|
/// <inheritdoc />
|
|
public DMApiParameters DMApiParameters => reattachInformation;
|
|
|
|
/// <inheritdoc />
|
|
public bool IsPrimary
|
|
{
|
|
get
|
|
{
|
|
CheckDisposed();
|
|
return reattachInformation.IsPrimary;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ApiValidationStatus ApiValidationStatus
|
|
{
|
|
get
|
|
{
|
|
if (!Lifetime.IsCompleted)
|
|
throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
|
|
return apiValidationStatus;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IDmbProvider Dmb
|
|
{
|
|
get
|
|
{
|
|
CheckDisposed();
|
|
return reattachInformation.Dmb;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ushort? Port
|
|
{
|
|
get
|
|
{
|
|
CheckDisposed();
|
|
if (portClosedForReboot)
|
|
return null;
|
|
return reattachInformation.Port;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public RebootState RebootState
|
|
{
|
|
get
|
|
{
|
|
CheckDisposed();
|
|
return reattachInformation.RebootState;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Version DMApiVersion { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public bool ClosePortOnReboot { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
public bool TerminationWasRequested { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public Task<LaunchResult> LaunchResult { get; }
|
|
|
|
/// <inheritdoc />
|
|
public Task<int> Lifetime => process.Lifetime;
|
|
|
|
/// <inheritdoc />
|
|
public Task OnReboot => rebootTcs.Task;
|
|
|
|
/// <inheritdoc />
|
|
public Task OnPrime => primeTcs.Task;
|
|
|
|
/// <summary>
|
|
/// The up to date <see cref="ReattachInformation"/>
|
|
/// </summary>
|
|
readonly ReattachInformation reattachInformation;
|
|
|
|
/// <summary>
|
|
/// The <see cref="ITopicClient"/> for the <see cref="SessionController"/>
|
|
/// </summary>
|
|
readonly ITopicClient byondTopicSender;
|
|
|
|
/// <summary>
|
|
/// The <see cref="IBridgeRegistration"/> for the <see cref="SessionController"/>
|
|
/// </summary>
|
|
readonly IBridgeRegistration bridgeRegistration;
|
|
|
|
/// <summary>
|
|
/// The <see cref="IProcess"/> for the <see cref="SessionController"/>
|
|
/// </summary>
|
|
readonly IProcess process;
|
|
|
|
/// <summary>
|
|
/// The <see cref="IByondExecutableLock"/> for the <see cref="SessionController"/>
|
|
/// </summary>
|
|
readonly IByondExecutableLock byondLock;
|
|
|
|
/// <summary>
|
|
/// The <see cref="IChatTrackingContext"/> for the <see cref="SessionController"/>
|
|
/// </summary>
|
|
readonly IChatTrackingContext chatTrackingContext;
|
|
|
|
/// <summary>
|
|
/// The <see cref="IChatManager"/> for the <see cref="SessionController"/>
|
|
/// </summary>
|
|
readonly IChatManager chat;
|
|
|
|
/// <summary>
|
|
/// The <see cref="ILogger"/> for the <see cref="SessionController"/>
|
|
/// </summary>
|
|
readonly ILogger<SessionController> logger;
|
|
|
|
/// <summary>
|
|
/// <see langword="lock"/> <see cref="object"/> for port updates and <see cref="disposed"/>.
|
|
/// </summary>
|
|
readonly object synchronizationLock;
|
|
|
|
/// <summary>
|
|
/// The <see cref="TaskCompletionSource{TResult}"/> <see cref="SetPort(ushort, CancellationToken)"/> waits on when DreamDaemon currently has it's ports closed
|
|
/// </summary>
|
|
TaskCompletionSource<bool> portAssignmentTcs;
|
|
|
|
/// <summary>
|
|
/// The port to assign DreamDaemon when it queries for it
|
|
/// </summary>
|
|
ushort? nextPort;
|
|
|
|
/// <summary>
|
|
/// The <see cref="TaskCompletionSource{TResult}"/> that completes when DD tells us about a reboot
|
|
/// </summary>
|
|
TaskCompletionSource<object> rebootTcs;
|
|
|
|
/// <summary>
|
|
/// The <see cref="TaskCompletionSource{TResult}"/> that completes when DD tells us it's primed.
|
|
/// </summary>
|
|
TaskCompletionSource<object> primeTcs;
|
|
|
|
/// <summary>
|
|
/// If we know DreamDaemon currently has it's port closed
|
|
/// </summary>
|
|
bool portClosedForReboot;
|
|
|
|
/// <summary>
|
|
/// If the <see cref="SessionController"/> has been disposed
|
|
/// </summary>
|
|
bool disposed;
|
|
|
|
/// <summary>
|
|
/// The <see cref="ApiValidationStatus"/> for the <see cref="SessionController"/>
|
|
/// </summary>
|
|
ApiValidationStatus apiValidationStatus;
|
|
|
|
/// <summary>
|
|
/// If <see cref="process"/> should be kept alive instead
|
|
/// </summary>
|
|
bool released;
|
|
|
|
/// <summary>
|
|
/// Construct a <see cref="SessionController"/>
|
|
/// </summary>
|
|
/// <param name="reattachInformation">The value of <see cref="reattachInformation"/></param>
|
|
/// <param name="process">The value of <see cref="process"/></param>
|
|
/// <param name="byondLock">The value of <see cref="byondLock"/></param>
|
|
/// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param>
|
|
/// <param name="bridgeRegistrar">The <see cref="IBridgeRegistrar"/> used to populate <see cref="bridgeRegistration"/>.</param>
|
|
/// <param name="chat">The value of <see cref="chat"/></param>
|
|
/// <param name="chatTrackingContext">The value of <see cref="chatTrackingContext"/></param>
|
|
/// <param name="logger">The value of <see cref="logger"/></param>
|
|
/// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/></param>
|
|
/// <param name="reattached">If this is a reattached session.</param>
|
|
public SessionController(
|
|
ReattachInformation reattachInformation,
|
|
IProcess process,
|
|
IByondExecutableLock byondLock,
|
|
ITopicClient byondTopicSender,
|
|
IChatTrackingContext chatTrackingContext,
|
|
IBridgeRegistrar bridgeRegistrar,
|
|
IChatManager chat,
|
|
ILogger<SessionController> logger,
|
|
uint? startupTimeout,
|
|
bool reattached)
|
|
{
|
|
this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
|
|
this.process = process ?? throw new ArgumentNullException(nameof(process));
|
|
this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock));
|
|
this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
|
|
this.chatTrackingContext = chatTrackingContext ?? throw new ArgumentNullException(nameof(chatTrackingContext));
|
|
bridgeRegistration = bridgeRegistrar?.RegisterHandler(this) ?? throw new ArgumentNullException(nameof(bridgeRegistrar));
|
|
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
|
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
|
|
this.chatTrackingContext.SetChannelSink(this);
|
|
|
|
portClosedForReboot = false;
|
|
disposed = false;
|
|
apiValidationStatus = ApiValidationStatus.NeverValidated;
|
|
released = false;
|
|
|
|
rebootTcs = new TaskCompletionSource<object>();
|
|
primeTcs = new TaskCompletionSource<object>();
|
|
|
|
synchronizationLock = new object();
|
|
|
|
CancellationTokenSource cts = null;
|
|
Task lifetimeContinuation = null;
|
|
lifetimeContinuation = process.Lifetime.ContinueWith(
|
|
x =>
|
|
{
|
|
lock (lifetimeContinuation)
|
|
cts?.Cancel();
|
|
chatTrackingContext.Active = false;
|
|
},
|
|
TaskScheduler.Current);
|
|
|
|
async Task<LaunchResult> GetLaunchResult()
|
|
{
|
|
var startTime = DateTimeOffset.Now;
|
|
Task toAwait = process.Startup;
|
|
|
|
if (startupTimeout.HasValue)
|
|
toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
|
|
|
|
await toAwait.ConfigureAwait(false);
|
|
|
|
var result = new LaunchResult
|
|
{
|
|
ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null,
|
|
StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
|
|
};
|
|
|
|
logger.LogTrace("Launch result: {0}", result);
|
|
|
|
if (!result.ExitCode.HasValue && reattached)
|
|
using (cts = new CancellationTokenSource())
|
|
try
|
|
{
|
|
await SendCommand(
|
|
new TopicParameters(
|
|
reattachInformation.RuntimeInformation.ServerPort,
|
|
true),
|
|
cts.Token)
|
|
.ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
lock (lifetimeContinuation)
|
|
cts = null;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
LaunchResult = GetLaunchResult();
|
|
|
|
logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finalizes an instance of the <see cref="SessionController"/> class.
|
|
/// </summary>
|
|
/// <remarks>The finalizer dispose pattern is necessary so we don't accidentally leak the executable</remarks>
|
|
#pragma warning disable CA1821 // Remove empty Finalizers TODO: remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
|
|
~SessionController() => Dispose(false);
|
|
#pragma warning restore CA1821 // Remove empty Finalizers
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Implements the <see cref="IDisposable"/> pattern
|
|
/// </summary>
|
|
/// <param name="disposing">If this function was NOT called by the finalizer</param>
|
|
void Dispose(bool disposing)
|
|
{
|
|
lock (synchronizationLock)
|
|
{
|
|
if (disposed)
|
|
return;
|
|
if (disposing)
|
|
{
|
|
if (!released)
|
|
{
|
|
process.Terminate();
|
|
byondLock.Dispose();
|
|
}
|
|
|
|
process.Dispose();
|
|
bridgeRegistration.Dispose();
|
|
Dmb?.Dispose(); // will be null when released
|
|
chatTrackingContext.Dispose();
|
|
disposed = true;
|
|
}
|
|
else
|
|
{
|
|
if (logger != null)
|
|
logger.LogError("Being disposed via finalizer!");
|
|
if (!released)
|
|
if (process != null)
|
|
process.Terminate();
|
|
else if (logger != null)
|
|
logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
|
|
{
|
|
if (parameters == null)
|
|
throw new ArgumentNullException(nameof(parameters));
|
|
|
|
var response = new BridgeResponse();
|
|
switch (parameters.CommandType)
|
|
{
|
|
case BridgeCommandType.ChatSend:
|
|
if (parameters.ChatMessage == null)
|
|
return new BridgeResponse
|
|
{
|
|
ErrorMessage = "Missing chatMessage field!"
|
|
};
|
|
|
|
if (parameters.ChatMessage.ChannelIds == null)
|
|
return new BridgeResponse
|
|
{
|
|
ErrorMessage = "Missing channelIds field in chatMessage!"
|
|
};
|
|
|
|
if(parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _)))
|
|
return new BridgeResponse
|
|
{
|
|
ErrorMessage = "Invalid channelIds in chatMessage!"
|
|
};
|
|
|
|
if (parameters.ChatMessage.Text == null)
|
|
return new BridgeResponse
|
|
{
|
|
ErrorMessage = "Missing message field in chatMessage!"
|
|
};
|
|
|
|
await chat.SendMessage(
|
|
parameters.ChatMessage.Text,
|
|
parameters.ChatMessage.ChannelIds.Select(UInt64.Parse),
|
|
cancellationToken).ConfigureAwait(false);
|
|
break;
|
|
case BridgeCommandType.Prime:
|
|
var oldPrimeTcs = primeTcs;
|
|
primeTcs = new TaskCompletionSource<object>();
|
|
oldPrimeTcs.SetResult(null);
|
|
break;
|
|
case BridgeCommandType.Kill:
|
|
logger.LogInformation("Bridge requested process termination!");
|
|
TerminationWasRequested = true;
|
|
process.Terminate();
|
|
break;
|
|
case BridgeCommandType.PortUpdate:
|
|
lock (synchronizationLock)
|
|
{
|
|
if (!parameters.CurrentPort.HasValue)
|
|
{
|
|
/////UHHHH
|
|
logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
|
|
return new BridgeResponse
|
|
{
|
|
ErrorMessage = "Missing stringified port as data parameter!"
|
|
};
|
|
}
|
|
|
|
var currentPort = parameters.CurrentPort.Value;
|
|
if (!nextPort.HasValue)
|
|
reattachInformation.Port = parameters.CurrentPort.Value; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to
|
|
else
|
|
{
|
|
// nextPort is ready, tell DD to switch to that
|
|
// if it fails it'll kill itself
|
|
response.NewPort = nextPort.Value;
|
|
reattachInformation.Port = nextPort.Value;
|
|
nextPort = null;
|
|
|
|
// we'll also get here from SetPort so complete that task
|
|
var tmpTcs = portAssignmentTcs;
|
|
portAssignmentTcs = null;
|
|
tmpTcs.SetResult(true);
|
|
}
|
|
|
|
portClosedForReboot = false;
|
|
}
|
|
|
|
break;
|
|
case BridgeCommandType.Startup:
|
|
apiValidationStatus = ApiValidationStatus.BadValidationRequest;
|
|
if (parameters.Version == null)
|
|
return new BridgeResponse
|
|
{
|
|
ErrorMessage = "Missing dmApiVersion field!"
|
|
};
|
|
|
|
DMApiVersion = parameters.Version;
|
|
switch (parameters.MinimumSecurityLevel)
|
|
{
|
|
case DreamDaemonSecurity.Ultrasafe:
|
|
apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
|
|
break;
|
|
case DreamDaemonSecurity.Safe:
|
|
apiValidationStatus = ApiValidationStatus.RequiresSafe;
|
|
break;
|
|
case DreamDaemonSecurity.Trusted:
|
|
apiValidationStatus = ApiValidationStatus.RequiresTrusted;
|
|
break;
|
|
case null:
|
|
return new BridgeResponse
|
|
{
|
|
ErrorMessage = "Missing minimumSecurityLevel field!"
|
|
};
|
|
default:
|
|
return new BridgeResponse
|
|
{
|
|
ErrorMessage = "Invalid minimumSecurityLevel!"
|
|
};
|
|
}
|
|
|
|
response.RuntimeInformation = reattachInformation.RuntimeInformation;
|
|
|
|
// Load custom commands
|
|
chatTrackingContext.CustomCommands = parameters.CustomCommands.ToList();
|
|
break;
|
|
case BridgeCommandType.Reboot:
|
|
if (ClosePortOnReboot)
|
|
{
|
|
chatTrackingContext.Active = false;
|
|
response.NewPort = 0;
|
|
portClosedForReboot = true;
|
|
}
|
|
|
|
var oldRebootTcs = rebootTcs;
|
|
rebootTcs = new TaskCompletionSource<object>();
|
|
oldRebootTcs.SetResult(null);
|
|
break;
|
|
case null:
|
|
response.ErrorMessage = "Missing commandType!";
|
|
break;
|
|
default:
|
|
response.ErrorMessage = "Requested commandType not supported!";
|
|
break;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Throws an <see cref="ObjectDisposedException"/> if <see cref="Dispose(bool)"/> has been called
|
|
/// </summary>
|
|
void CheckDisposed()
|
|
{
|
|
if (disposed)
|
|
throw new ObjectDisposedException(nameof(SessionController));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void EnableCustomChatCommands() => chatTrackingContext.Active = true;
|
|
|
|
/// <inheritdoc />
|
|
public ReattachInformation Release()
|
|
{
|
|
CheckDisposed();
|
|
|
|
// we still don't want to dispose the dmb yet, even though we're keeping it alive
|
|
var tmpProvider = reattachInformation.Dmb;
|
|
reattachInformation.Dmb = null;
|
|
released = true;
|
|
Dispose();
|
|
byondLock.DoNotDeleteThisSession();
|
|
tmpProvider.KeepAlive();
|
|
reattachInformation.Dmb = tmpProvider;
|
|
return reattachInformation;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<CombinedTopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
|
|
{
|
|
if (Lifetime.IsCompleted)
|
|
{
|
|
logger.LogWarning(
|
|
"Attempted to send a command to an inactive SessionController: {0}",
|
|
parameters.CommandType);
|
|
return null;
|
|
}
|
|
|
|
parameters.AccessIdentifier = reattachInformation.AccessIdentifier;
|
|
|
|
var json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings);
|
|
logger.LogTrace("Topic request: {0}", json);
|
|
try
|
|
{
|
|
var commandString = String.Format(CultureInfo.InvariantCulture,
|
|
"?{0}={1}",
|
|
byondTopicSender.SanitizeString(DMApiConstants.TopicData),
|
|
byondTopicSender.SanitizeString(json));
|
|
|
|
var targetPort = reattachInformation.Port;
|
|
|
|
var topicResponse = await byondTopicSender.SendTopic(
|
|
new IPEndPoint(IPAddress.Loopback, targetPort),
|
|
commandString,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
var topicReturn = topicResponse.StringData;
|
|
|
|
Interop.Topic.TopicResponse interopResponse = null;
|
|
if (topicReturn != null)
|
|
try
|
|
{
|
|
interopResponse = JsonConvert.DeserializeObject<Interop.Topic.TopicResponse>(topicReturn, DMApiConstants.SerializerSettings);
|
|
if (interopResponse.ErrorMessage != null)
|
|
{
|
|
logger.LogWarning("Errored topic response for command {0}: {1}", parameters.CommandType, interopResponse.ErrorMessage);
|
|
}
|
|
|
|
logger.LogTrace("Interop response: {0}", topicReturn);
|
|
}
|
|
catch
|
|
{
|
|
logger.LogWarning("Invalid interop response: {0}", topicReturn);
|
|
}
|
|
|
|
return new CombinedTopicResponse(topicResponse, interopResponse);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
logger.LogTrace(
|
|
"Topic request {0}!",
|
|
cancellationToken.IsCancellationRequested
|
|
? "aborted"
|
|
: "timed out");
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.LogWarning("Send command exception:{0}{1}", Environment.NewLine, e);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task<bool> SetPort(ushort port, CancellationToken cancellationToken)
|
|
{
|
|
CheckDisposed();
|
|
|
|
if (port == 0)
|
|
throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
|
|
|
|
async Task<bool> ImmediateTopicPortChange()
|
|
{
|
|
var commandResult = await SendCommand(
|
|
new TopicParameters(port, false),
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (commandResult.InteropResponse?.ErrorMessage != null)
|
|
return false;
|
|
|
|
reattachInformation.Port = port;
|
|
return true;
|
|
}
|
|
|
|
lock (synchronizationLock)
|
|
if (portClosedForReboot)
|
|
{
|
|
if (portAssignmentTcs != null)
|
|
throw new InvalidOperationException("A port change operation is already in progress!");
|
|
nextPort = port;
|
|
portAssignmentTcs = new TaskCompletionSource<bool>();
|
|
return portAssignmentTcs.Task;
|
|
}
|
|
else
|
|
return ImmediateTopicPortChange();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
|
|
{
|
|
if (RebootState == newRebootState)
|
|
return true;
|
|
|
|
logger.LogTrace("Changing reboot state to {0}", newRebootState);
|
|
|
|
reattachInformation.RebootState = newRebootState;
|
|
var result = await SendCommand(
|
|
new TopicParameters(newRebootState),
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
return result?.InteropResponse != null && result.InteropResponse?.ErrorMessage == null;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void ResetRebootState()
|
|
{
|
|
CheckDisposed();
|
|
logger.LogTrace("Resetting reboot state...");
|
|
reattachInformation.RebootState = RebootState.Normal;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetHighPriority() => process.SetHighPriority();
|
|
|
|
/// <inheritdoc />
|
|
public void Suspend() => process.Suspend();
|
|
|
|
/// <inheritdoc />
|
|
public void Resume() => process.Resume();
|
|
|
|
/// <inheritdoc />
|
|
public void ReplaceDmbProvider(IDmbProvider dmbProvider)
|
|
{
|
|
var oldDmb = reattachInformation.Dmb;
|
|
reattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
|
|
oldDmb.Dispose();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
|
|
=> SendCommand(new TopicParameters(newInstanceName), cancellationToken);
|
|
|
|
/// <inheritdoc />
|
|
public Task UpdateChannels(IEnumerable<ChannelRepresentation> newChannels, CancellationToken cancellationToken)
|
|
=> SendCommand(
|
|
new TopicParameters(
|
|
new ChatUpdate(newChannels)),
|
|
cancellationToken);
|
|
}
|
|
}
|