Introduces IConnectionFilter and the ConnectionFilters registry: one seam the accept path consults per inbound socket, so core no longer has to know where a gate's data comes from. The registry is a plain array walked by an indexed loop, so the hot path has no enumerator, no closure and no allocation; an interface dispatch is noise next to the accept syscall. Filters register during the Configure sweep, cheapest first, and the first denial short-circuits. A filter that throws on the accept path is unregistered and the connection fails open. A filter that faults once faults for every subsequent connection, so leaving it registered would mean an exception and a log line per accept -- exactly the amplification an attacker wants -- and a broken filter must not be able to deny every connection either. With that seam in place the whole file blocklist moves to UOContent: it is policy (which feeds, when to promote, what to report) built on an external file format with an external producer, and core does not need any of it. Firewall stays in core -- it is long-standing public API, it is what an admin reaches for manually, and a shard running without UOContent still has to be able to block an address -- but it now reaches the accept path through the same registry via a small adapter, so the two remain separate implementations rather than one conflated store. Blocklist policy moves out of bans.json into a UOContent Configuration/ blocklist.json, next to crowdsec.json. bans.json keeps only what core decides: reportRateLimitTrips and autoBanDuration. Cleanups found while moving the code: - BanChannel.Stop() persisted the Firewall. A contribution coordinator has no business saving an enforcement store; that is now the firewall filter's Stop. - BlocklistGate.Evaluate took a `whitelisted` flag that was hardcoded false at its only call site, and no whitelist concept exists anywhere in core. Dropped. - FileBlocklist was a static holding a snapshot and a promote-guard, which forced its tests onto the sequential collection and a LoadForTesting reset hook. It is now an instance, so each test owns its own state and they run in parallel. BlocklistGate folds into it: the pure decision survives as an internal Evaluate, which is all the separate type ever provided. - The promote-guard sweep timer was registered from NetState.Configure, two files from the guard it swept, and ran even with the blocklist disabled. It now starts with the filter that owns it. Core sheds ~570 source and ~340 test lines for ~90 of seam. 1344 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
669 lines
20 KiB
C#
669 lines
20 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: Main.cs *
|
|
* *
|
|
* This program is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
*************************************************************************/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Server.Compression;
|
|
using Server.Json;
|
|
using Server.Logging;
|
|
using Server.Network;
|
|
using Server.Network.Bans;
|
|
using Server.Text;
|
|
|
|
namespace Server;
|
|
|
|
public static class Core
|
|
{
|
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(Core));
|
|
|
|
private static bool _performProcessKill;
|
|
private static bool _restartOnKill;
|
|
private static bool _performSnapshot;
|
|
private static string _snapshotPath;
|
|
private static bool _crashed;
|
|
private static string _baseDirectory;
|
|
|
|
private static bool? _isRunningFromXUnit;
|
|
|
|
private static int _itemCount;
|
|
private static int _mobileCount;
|
|
public static EventLoopContext LoopContext { get; set; }
|
|
|
|
private static readonly Type[] _serialTypeArray = { typeof(Serial) };
|
|
|
|
public static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
|
public static readonly bool IsDarwin = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
|
public static readonly bool IsFreeBSD = RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD);
|
|
public static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || IsFreeBSD;
|
|
public static readonly bool IsBSD = IsDarwin || IsFreeBSD;
|
|
public static readonly bool Unix = IsBSD || IsLinux;
|
|
|
|
private const string AssembliesConfiguration = "Data/assemblies.json";
|
|
|
|
#nullable enable
|
|
// TODO: Find a way to get rid of this
|
|
public static bool IsRunningFromXUnit
|
|
{
|
|
get
|
|
{
|
|
if (_isRunningFromXUnit != null)
|
|
{
|
|
return _isRunningFromXUnit.Value;
|
|
}
|
|
|
|
foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
|
|
{
|
|
if (a.FullName.InsensitiveStartsWith("xunit"))
|
|
{
|
|
_isRunningFromXUnit = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
_isRunningFromXUnit = false;
|
|
return false;
|
|
}
|
|
}
|
|
#nullable restore
|
|
|
|
public static Assembly ApplicationAssembly { get; set; }
|
|
public static Assembly Assembly { get; set; }
|
|
|
|
// Assembly file version
|
|
public static Version Version => new(ThisAssembly.AssemblyFileVersion);
|
|
|
|
public static Process Process { get; private set; }
|
|
|
|
public static Thread Thread { get; private set; }
|
|
|
|
private static long _firstTick;
|
|
|
|
// Make these available to unit tests for mocking
|
|
internal static long _tickCount;
|
|
internal static DateTime _now;
|
|
|
|
public static long TickCount => _tickCount;
|
|
|
|
public static DateTime Now => _now;
|
|
|
|
public static long Uptime => TickCount - _firstTick;
|
|
|
|
private static double _currentCPS;
|
|
private static double _averageCPS;
|
|
private static bool _cpsInitialized;
|
|
|
|
public static double CyclesPerSecond => _currentCPS;
|
|
|
|
public static double AverageCPS => _averageCPS;
|
|
|
|
public static string BaseDirectory
|
|
{
|
|
get
|
|
{
|
|
if (_baseDirectory == null)
|
|
{
|
|
try
|
|
{
|
|
_baseDirectory = ApplicationAssembly.Location;
|
|
|
|
if (_baseDirectory.Length > 0)
|
|
{
|
|
_baseDirectory = Path.GetDirectoryName(_baseDirectory);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
_baseDirectory = "";
|
|
}
|
|
}
|
|
|
|
return _baseDirectory;
|
|
}
|
|
}
|
|
|
|
public static CancellationTokenSource ClosingTokenSource { get; } = new();
|
|
|
|
public static bool Closing => ClosingTokenSource.IsCancellationRequested;
|
|
|
|
public static bool Headless { get; private set; }
|
|
|
|
public static int GlobalUpdateRange { get; set; } = 18;
|
|
|
|
public static int GlobalMaxUpdateRange { get; set; } = 24;
|
|
|
|
public static int ScriptItems => _itemCount;
|
|
public static int ScriptMobiles => _mobileCount;
|
|
|
|
public static Expansion Expansion { get; set; }
|
|
public static bool T2A => Expansion >= Expansion.T2A;
|
|
|
|
public static bool UOR => Expansion >= Expansion.UOR;
|
|
|
|
public static bool UOTD => Expansion >= Expansion.UOTD;
|
|
|
|
public static bool LBR => Expansion >= Expansion.LBR;
|
|
|
|
public static bool AOS => Expansion >= Expansion.AOS;
|
|
|
|
public static bool SE => Expansion >= Expansion.SE;
|
|
|
|
public static bool ML => Expansion >= Expansion.ML;
|
|
|
|
public static bool SA => Expansion >= Expansion.SA;
|
|
|
|
public static bool HS => Expansion >= Expansion.HS;
|
|
|
|
public static bool TOL => Expansion >= Expansion.TOL;
|
|
|
|
public static bool EJ => Expansion >= Expansion.EJ;
|
|
|
|
public static string FindDataFile(string path, bool throwNotFound = true)
|
|
{
|
|
string fullPath = null;
|
|
|
|
foreach (var p in ServerConfiguration.DataDirectories)
|
|
{
|
|
fullPath = Path.Combine(p, path);
|
|
|
|
if (IsLinux && !File.Exists(fullPath))
|
|
{
|
|
var fi = new FileInfo(fullPath);
|
|
if (fi.Directory != null && Directory.Exists(fi.Directory.FullName))
|
|
{
|
|
fullPath = fi.Directory.EnumerateFiles(
|
|
fi.Name,
|
|
new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }
|
|
).FirstOrDefault()?.FullName;
|
|
}
|
|
}
|
|
|
|
if (File.Exists(fullPath))
|
|
{
|
|
break;
|
|
}
|
|
|
|
fullPath = null;
|
|
}
|
|
|
|
if (fullPath == null && throwNotFound)
|
|
{
|
|
throw new FileNotFoundException($"Data: {path} was not found");
|
|
}
|
|
|
|
return fullPath;
|
|
}
|
|
|
|
public static IEnumerable<string> FindDataFileByPattern(string pattern)
|
|
{
|
|
var options = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive };
|
|
foreach (var p in ServerConfiguration.DataDirectories)
|
|
{
|
|
if (Directory.Exists(p))
|
|
{
|
|
foreach (var file in Directory.EnumerateFiles(p, pattern, options))
|
|
{
|
|
yield return file;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void Kill(bool restart = false)
|
|
{
|
|
_restartOnKill = restart;
|
|
_performProcessKill = true;
|
|
}
|
|
|
|
public static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
|
{
|
|
Console.WriteLine(e.IsTerminating ? "Error:" : "Warning:");
|
|
Console.WriteLine(e.ExceptionObject);
|
|
|
|
if (e.IsTerminating)
|
|
{
|
|
_crashed = true;
|
|
|
|
var close = false;
|
|
|
|
try
|
|
{
|
|
var args = new ServerCrashedEventArgs(e.ExceptionObject as Exception);
|
|
|
|
EventSink.InvokeServerCrashed(args);
|
|
|
|
close = args.Close;
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
|
|
if (!close && !Headless)
|
|
{
|
|
Console.WriteLine("This exception is fatal, press return to exit");
|
|
ConsoleInputHandler.ReadLine();
|
|
}
|
|
|
|
DoKill();
|
|
}
|
|
}
|
|
|
|
private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
|
|
{
|
|
if (!Closing)
|
|
{
|
|
HandleClosed();
|
|
}
|
|
}
|
|
|
|
private static void Console_CancelKeyPressed(object sender, ConsoleCancelEventArgs e)
|
|
{
|
|
var keypress = e.SpecialKey switch
|
|
{
|
|
ConsoleSpecialKey.ControlBreak => "CTRL+BREAK",
|
|
_ => "CTRL+C"
|
|
};
|
|
|
|
logger.Information("Detected {Key} pressed.", keypress);
|
|
e.Cancel = true;
|
|
Kill();
|
|
}
|
|
|
|
internal static void DoKill(bool restart = false)
|
|
{
|
|
if (Closing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
HandleClosed();
|
|
|
|
if (restart)
|
|
{
|
|
try
|
|
{
|
|
logger.Information("Restarting");
|
|
if (IsWindows)
|
|
{
|
|
using var process = Process.Start("dotnet", $"{ApplicationAssembly.Location}");
|
|
}
|
|
else
|
|
{
|
|
using var process = new Process();
|
|
process.StartInfo = new ProcessStartInfo
|
|
{
|
|
FileName = "dotnet",
|
|
Arguments = $"{ApplicationAssembly.Location}",
|
|
UseShellExecute = true
|
|
};
|
|
|
|
process.Start();
|
|
}
|
|
logger.Information("Restart done");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error(e, "Restart failed");
|
|
}
|
|
}
|
|
|
|
Environment.Exit(0);
|
|
}
|
|
|
|
private static void HandleClosed()
|
|
{
|
|
ClosingTokenSource.Cancel();
|
|
|
|
logger.Information("Shutting down");
|
|
|
|
World.WaitForWriteCompletion();
|
|
World.ExitSerializationThreads();
|
|
PingServer.Shutdown();
|
|
NetState.Shutdown();
|
|
BanChannel.Stop();
|
|
ConnectionFilters.Stop();
|
|
|
|
if (!_crashed)
|
|
{
|
|
EventSink.InvokeShutdown();
|
|
}
|
|
}
|
|
|
|
private static readonly bool UseFastTimestampMath = Stopwatch.Frequency % 1000 == 0;
|
|
private static readonly ulong FrequencyInMilliseconds = (ulong)Stopwatch.Frequency / 1000;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static long GetTimestamp()
|
|
{
|
|
if (UseFastTimestampMath)
|
|
{
|
|
return (long)((ulong)Stopwatch.GetTimestamp() / FrequencyInMilliseconds);
|
|
}
|
|
|
|
// Fast calculation will be lossy, fallback to slower but accurate calculation
|
|
return (long)((UInt128)Stopwatch.GetTimestamp() * 1000 / (ulong)Stopwatch.Frequency);
|
|
}
|
|
|
|
public static void Setup(Assembly applicationAssembly, Process process)
|
|
{
|
|
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
|
|
|
Process = process;
|
|
ApplicationAssembly = applicationAssembly;
|
|
Assembly = Assembly.GetAssembly(typeof(Core));
|
|
Thread = Thread.CurrentThread;
|
|
LoopContext = new EventLoopContext();
|
|
SynchronizationContext.SetSynchronizationContext(LoopContext);
|
|
|
|
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
|
AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
|
|
AppDomain.CurrentDomain.AssemblyResolve += AssemblyHandler.AssemblyResolver;
|
|
|
|
Console.OutputEncoding = Encoding.UTF8;
|
|
Thread.Name = "Core Thread";
|
|
|
|
if (BaseDirectory.Length > 0)
|
|
{
|
|
Directory.SetCurrentDirectory(BaseDirectory);
|
|
}
|
|
|
|
Utility.PushColor(ConsoleColor.Green);
|
|
Console.WriteLine(
|
|
"ModernUO - [https://github.com/modernuo/modernuo] Version {0}.{1}.{2}.{3}",
|
|
Version.Major,
|
|
Version.Minor,
|
|
Version.Build,
|
|
Version.Revision
|
|
);
|
|
Utility.PopColor();
|
|
|
|
Utility.PushColor(ConsoleColor.DarkGray);
|
|
Console.WriteLine(@"Copyright 2019-2026 ModernUO Development Team
|
|
This program comes with ABSOLUTELY NO WARRANTY;
|
|
This is free software, and you are welcome to redistribute it under certain conditions.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
".TrimMultiline());
|
|
Utility.PopColor();
|
|
|
|
Console.CancelKeyPress += Console_CancelKeyPressed;
|
|
|
|
Headless = Console.IsInputRedirected;
|
|
if (Headless)
|
|
{
|
|
logger.Information("Headless mode detected (stdin is not a TTY); interactive console input is disabled.");
|
|
}
|
|
|
|
// LibDeflate is not thread safe, so we need to create a new instance for each thread
|
|
var standard = Deflate.Standard;
|
|
AppDomain.CurrentDomain.ProcessExit += (_, _) => standard.Dispose();
|
|
|
|
ServerConfiguration.Load();
|
|
|
|
var assemblyPath = Path.Join(BaseDirectory, AssembliesConfiguration);
|
|
|
|
// Load UOContent.dll
|
|
var assemblyFiles = JsonConfig.Deserialize<List<string>>(assemblyPath)?.ToArray();
|
|
if (assemblyFiles == null)
|
|
{
|
|
throw new JsonException($"Failed to deserialize {assemblyPath}.");
|
|
}
|
|
|
|
for (var i = 0; i < assemblyFiles.Length; i++)
|
|
{
|
|
assemblyFiles[i] = Path.Join(BaseDirectory, "Assemblies", assemblyFiles[i]);
|
|
}
|
|
|
|
AssemblyHandler.LoadAssemblies(assemblyFiles);
|
|
|
|
// First-boot interactive setup. Runs after assemblies are loaded (so content can
|
|
// register prompts) but before any Serilog output, so console prompts are not
|
|
// interleaved with the async console sink. Handlers self-gate on first-boot state
|
|
// (e.g. "is my setting already present?").
|
|
AssemblyHandler.Invoke("ConfigurePrompts");
|
|
|
|
logger.Information("Running on {Framework}", RuntimeInformation.FrameworkDescription);
|
|
|
|
VerifySerialization();
|
|
|
|
_now = DateTime.UtcNow;
|
|
_firstTick = _tickCount = GetTimestamp();
|
|
|
|
Timer.Init(_tickCount);
|
|
|
|
AssemblyHandler.Invoke("Configure");
|
|
|
|
TileMatrixLoader.LoadTileMatrix();
|
|
|
|
RegionJsonSerializer.LoadRegions();
|
|
World.Load();
|
|
|
|
AssemblyHandler.Invoke("Initialize");
|
|
|
|
BanChannel.Start(ClosingTokenSource.Token);
|
|
ConnectionFilters.Start(ClosingTokenSource.Token);
|
|
NetState.Start();
|
|
PingServer.Start();
|
|
EventSink.InvokeServerStarted();
|
|
RunEventLoop();
|
|
}
|
|
|
|
public static void RunEventLoop()
|
|
{
|
|
try
|
|
{
|
|
var lastRaw = Stopwatch.GetTimestamp();
|
|
const int interval = 100;
|
|
double frequency = Stopwatch.Frequency * interval;
|
|
const double alpha = 2.0 / 129; // EMA smoothing (≈128-sample window)
|
|
|
|
var sample = 0;
|
|
|
|
while (!Closing)
|
|
{
|
|
_tickCount = GetTimestamp();
|
|
_now = DateTime.UtcNow;
|
|
|
|
Mobile.ProcessDeltaQueue();
|
|
Item.ProcessDeltaQueue();
|
|
Timer.Slice(_tickCount);
|
|
|
|
// Handle networking
|
|
NetState.Slice();
|
|
|
|
// Execute captured post-await methods (like Timer.Pause)
|
|
LoopContext.ExecuteTasks();
|
|
|
|
Timer.CheckTimerPool(); // Check for pool depletion so we can async refill it.
|
|
|
|
if (_performSnapshot)
|
|
{
|
|
// Return value is the offset that can be used to fix timers that should drift
|
|
World.Snapshot(_snapshotPath);
|
|
_performSnapshot = false;
|
|
}
|
|
|
|
if (_performProcessKill)
|
|
{
|
|
World.WaitForWriteCompletion();
|
|
break;
|
|
}
|
|
|
|
if (sample++ == interval)
|
|
{
|
|
sample = 0;
|
|
var nowRaw = Stopwatch.GetTimestamp();
|
|
|
|
_currentCPS = frequency / (nowRaw - lastRaw);
|
|
|
|
if (!_cpsInitialized)
|
|
{
|
|
_averageCPS = _currentCPS;
|
|
_cpsInitialized = true;
|
|
}
|
|
else
|
|
{
|
|
_averageCPS += alpha * (_currentCPS - _averageCPS);
|
|
}
|
|
|
|
lastRaw = nowRaw;
|
|
|
|
var sleepMs = (int)Timer.MillisecondsUntilNextTick(_tickCount);
|
|
if (sleepMs >= 2)
|
|
{
|
|
NetState.WaitForCompletion(sleepMs - 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
CurrentDomain_UnhandledException(null, new UnhandledExceptionEventArgs(e, true));
|
|
return;
|
|
}
|
|
|
|
DoKill(_restartOnKill);
|
|
}
|
|
|
|
internal static void RequestSnapshot(string snapshotPath)
|
|
{
|
|
_snapshotPath = snapshotPath;
|
|
_performSnapshot = true;
|
|
}
|
|
|
|
public static void VerifySerialization()
|
|
{
|
|
_itemCount = 0;
|
|
_mobileCount = 0;
|
|
|
|
var callingAssembly = Assembly.GetCallingAssembly();
|
|
|
|
VerifySerialization(callingAssembly);
|
|
|
|
foreach (var assembly in AssemblyHandler.Assemblies)
|
|
{
|
|
if (assembly != callingAssembly)
|
|
{
|
|
VerifySerialization(assembly);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void VerifyType(Type type)
|
|
{
|
|
if (!type.IsAssignableTo(typeof(ISerializable)) || type.IsInterface || type.IsAbstract)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (type.IsSubclassOf(typeof(Item)))
|
|
{
|
|
Interlocked.Increment(ref _itemCount);
|
|
}
|
|
else if (type.IsSubclassOf(typeof(Mobile)))
|
|
{
|
|
Interlocked.Increment(ref _mobileCount);
|
|
}
|
|
|
|
using var errors = ValueStringBuilder.CreateMT();
|
|
|
|
try
|
|
{
|
|
if (World.DirtyTrackingEnabled)
|
|
{
|
|
var manualDirtyCheckingAttribute = type.GetCustomAttribute<ManualDirtyCheckingAttribute>(false);
|
|
var codeGennedAttribute = type.GetCustomAttribute<ModernUO.Serialization.SerializationGeneratorAttribute>(false);
|
|
|
|
if (manualDirtyCheckingAttribute == null && codeGennedAttribute == null)
|
|
{
|
|
errors.AppendLine(" - No property tracking (dirty checking)");
|
|
}
|
|
}
|
|
|
|
if (type.GetConstructor(_serialTypeArray) == null)
|
|
{
|
|
errors.AppendLine(" - No serialization constructor");
|
|
}
|
|
|
|
const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic |
|
|
BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
|
|
|
var hasSerializeMethod = false;
|
|
var hasDeserializeMethod = false;
|
|
|
|
foreach (var method in type.GetMethods(bindingFlags))
|
|
{
|
|
if (method.Name == "Serialize")
|
|
{
|
|
hasSerializeMethod = true;
|
|
}
|
|
|
|
if (method.Name == "Deserialize")
|
|
{
|
|
var parameters = method.GetParameters();
|
|
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(IGenericReader))
|
|
{
|
|
hasDeserializeMethod = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasSerializeMethod)
|
|
{
|
|
errors.AppendLine(" - No Serialize() method");
|
|
}
|
|
|
|
if (!hasDeserializeMethod)
|
|
{
|
|
errors.AppendLine(" - No Deserialize() method");
|
|
}
|
|
|
|
if (errors.Length > 0)
|
|
{
|
|
Utility.PushColor(ConsoleColor.Red);
|
|
Console.WriteLine($"{type}{Environment.NewLine}{errors.ToString()}");
|
|
Utility.PopColor();
|
|
}
|
|
}
|
|
catch (AmbiguousMatchException e)
|
|
{
|
|
// ignored
|
|
}
|
|
catch
|
|
{
|
|
Console.WriteLine("Warning: Exception in serialization verification of type {0}", type);
|
|
}
|
|
}
|
|
|
|
private static void VerifySerialization(Assembly assembly)
|
|
{
|
|
if (assembly != null)
|
|
{
|
|
Parallel.ForEach(assembly.GetTypes(), VerifyType);
|
|
}
|
|
}
|
|
}
|