report a peer's port change locally without rewriting it
Keep IConfigurationManager source-compatible for plugin implementers.
This commit is contained in:
@@ -792,6 +792,43 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Works out what a configuration update means for the ports this process bound at startup.
|
||||
/// </summary>
|
||||
/// <param name="boundHttpPort">The HTTP port this process is bound to.</param>
|
||||
/// <param name="boundHttpsPort">The HTTPS port this process is bound to.</param>
|
||||
/// <param name="configuredHttpPort">The HTTP port the shared configuration now carries.</param>
|
||||
/// <param name="configuredHttpsPort">The HTTPS port the shared configuration now carries.</param>
|
||||
/// <param name="isPortAuthorized">Whether the shared configuration still marks the port as authorized.</param>
|
||||
/// <param name="isApplyingRemoteInvalidation">Whether this update is another instance's write being applied.</param>
|
||||
/// <returns>What the update requires of this instance.</returns>
|
||||
internal static PortChangeOutcome EvaluatePortChange(
|
||||
int boundHttpPort,
|
||||
int boundHttpsPort,
|
||||
int configuredHttpPort,
|
||||
int configuredHttpsPort,
|
||||
bool isPortAuthorized,
|
||||
bool isApplyingRemoteInvalidation)
|
||||
{
|
||||
// Nothing is bound yet, so nothing has gone stale.
|
||||
if (boundHttpPort == 0 || boundHttpsPort == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (configuredHttpPort == boundHttpPort && configuredHttpsPort == boundHttpsPort)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
// Whoever wrote the change, this process is still listening on a port the configuration no
|
||||
// longer names, so the pending restart is reported either way. The authorization flag belongs
|
||||
// to the instance that made the change: it cleared the flag along with the port, and clearing
|
||||
// it again here would write shared configuration on that instance's behalf and announce it a
|
||||
// second time.
|
||||
return new PortChangeOutcome(true, isPortAuthorized && !isApplyingRemoteInvalidation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when [configuration updated].
|
||||
/// </summary>
|
||||
@@ -799,28 +836,24 @@ namespace Emby.Server.Implementations
|
||||
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
|
||||
private void OnConfigurationUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var requiresRestart = false;
|
||||
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
||||
|
||||
// Don't do anything if these haven't been set yet, and don't clear the authorization flag on
|
||||
// behalf of another instance: it already wrote that flag along with the port change, so
|
||||
// repeating the save here only races it.
|
||||
if (HttpPort != 0 && HttpsPort != 0 && !ConfigurationInvalidationContext.IsApplyingRemoteInvalidation)
|
||||
{
|
||||
// Need to restart if ports have changed
|
||||
if (networkConfiguration.InternalHttpPort != HttpPort
|
||||
|| networkConfiguration.InternalHttpsPort != HttpsPort)
|
||||
{
|
||||
if (ConfigurationManager.Configuration.IsPortAuthorized)
|
||||
{
|
||||
ConfigurationManager.Configuration.IsPortAuthorized = false;
|
||||
ConfigurationManager.SaveConfiguration();
|
||||
var portChange = EvaluatePortChange(
|
||||
HttpPort,
|
||||
HttpsPort,
|
||||
networkConfiguration.InternalHttpPort,
|
||||
networkConfiguration.InternalHttpsPort,
|
||||
ConfigurationManager.Configuration.IsPortAuthorized,
|
||||
ConfigurationInvalidationContext.IsApplyingRemoteInvalidation);
|
||||
|
||||
requiresRestart = true;
|
||||
}
|
||||
}
|
||||
if (portChange.ClearsPortAuthorization)
|
||||
{
|
||||
ConfigurationManager.Configuration.IsPortAuthorized = false;
|
||||
ConfigurationManager.SaveConfiguration();
|
||||
}
|
||||
|
||||
var requiresRestart = portChange.RequiresRestart;
|
||||
|
||||
if (ValidateSslCertificate(networkConfiguration))
|
||||
{
|
||||
requiresRestart = true;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Emby.Server.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// What a configuration update carrying different ports requires of the instance reading it.
|
||||
/// </summary>
|
||||
/// <param name="RequiresRestart">
|
||||
/// Whether this process is still bound to a port the shared configuration no longer names, and so has
|
||||
/// to report a pending restart.
|
||||
/// </param>
|
||||
/// <param name="ClearsPortAuthorization">
|
||||
/// Whether this instance is the one that has to clear the port authorization flag and save it.
|
||||
/// </param>
|
||||
internal readonly record struct PortChangeOutcome(bool RequiresRestart, bool ClearsPortAuthorization);
|
||||
}
|
||||
@@ -90,8 +90,15 @@ namespace MediaBrowser.Common.Configuration
|
||||
/// Drops the locally cached copy of configuration another instance has written to the shared
|
||||
/// configuration directory, so the next read reloads it, and raises the local update event.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An implementation predating the invalidation bus keeps the default, which reports that it
|
||||
/// cannot drop its cache rather than quietly leaving it stale. The caller applying a remote
|
||||
/// notice treats that as a failed apply and logs it.
|
||||
/// </remarks>
|
||||
/// <param name="key">The named configuration key, or <c>null</c> for the system configuration.</param>
|
||||
void InvalidateCachedConfiguration(string? key);
|
||||
/// <exception cref="NotSupportedException">The implementation cannot drop its cached configuration.</exception>
|
||||
void InvalidateCachedConfiguration(string? key)
|
||||
=> throw new NotSupportedException(GetType().Name + " cannot drop configuration cached from the shared configuration directory, so writes by other instances will not be picked up.");
|
||||
}
|
||||
|
||||
public static class ConfigurationManagerExtensions
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
using Emby.Server.Implementations;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// The decision <c>ApplicationHost.OnConfigurationUpdated</c> makes about a port change. The ports this
|
||||
/// process bound are fixed for its lifetime and the pending-restart flag is per-process, so an instance
|
||||
/// applying another instance's port change still has to notice its own binding went stale - while leaving
|
||||
/// the authorization write, and the notice that follows it, to the instance that made the change.
|
||||
/// </summary>
|
||||
public static class ApplicationHostPortChangeTests
|
||||
{
|
||||
/// <summary>
|
||||
/// The local case, unchanged: clear the authorization flag and report the pending restart.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public static void LocalPortChange_ClearsAuthorizationAndRequiresRestart()
|
||||
{
|
||||
var outcome = ApplicationHost.EvaluatePortChange(8096, 8920, 9096, 8920, true, false);
|
||||
|
||||
Assert.True(outcome.RequiresRestart);
|
||||
Assert.True(outcome.ClearsPortAuthorization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The cross-instance case: the peer wrote the new port and cleared the flag with it, so this
|
||||
/// instance must not write, but it is still listening on the old port and has to say so.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public static void RemotePortChange_RequiresRestartWithoutWriting(bool isPortAuthorized)
|
||||
{
|
||||
var outcome = ApplicationHost.EvaluatePortChange(8096, 8920, 9096, 8920, isPortAuthorized, true);
|
||||
|
||||
Assert.True(outcome.RequiresRestart);
|
||||
Assert.False(outcome.ClearsPortAuthorization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A second update while a port change is already pending must not write the flag again, and the
|
||||
/// binding is still stale.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public static void LocalPortChange_WithAuthorizationAlreadyCleared_RequiresRestartWithoutWriting()
|
||||
{
|
||||
var outcome = ApplicationHost.EvaluatePortChange(8096, 8920, 9096, 8920, false, false);
|
||||
|
||||
Assert.True(outcome.RequiresRestart);
|
||||
Assert.False(outcome.ClearsPortAuthorization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An update that leaves the ports alone is not a port change, whoever wrote it.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public static void UnchangedPorts_DoNothing(bool isApplyingRemoteInvalidation)
|
||||
{
|
||||
var outcome = ApplicationHost.EvaluatePortChange(8096, 8920, 8096, 8920, true, isApplyingRemoteInvalidation);
|
||||
|
||||
Assert.False(outcome.RequiresRestart);
|
||||
Assert.False(outcome.ClearsPortAuthorization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Nothing is decided before the ports have been bound.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(0, 8920)]
|
||||
[InlineData(8096, 0)]
|
||||
public static void UnboundPorts_DoNothing(int boundHttpPort, int boundHttpsPort)
|
||||
{
|
||||
var outcome = ApplicationHost.EvaluatePortChange(boundHttpPort, boundHttpsPort, 9096, 9920, true, false);
|
||||
|
||||
Assert.False(outcome.RequiresRestart);
|
||||
Assert.False(outcome.ClearsPortAuthorization);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user