first commit
This commit is contained in:
602
NitroxServer-Subnautica/Program.cs
Normal file
602
NitroxServer-Subnautica/Program.cs
Normal file
@@ -0,0 +1,602 @@
|
||||
global using NitroxModel.Logger;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NitroxModel;
|
||||
using NitroxModel.Core;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxServer;
|
||||
using NitroxServer_Subnautica.Communication;
|
||||
using NitroxServer.ConsoleCommands.Processor;
|
||||
|
||||
namespace NitroxServer_Subnautica;
|
||||
|
||||
[SuppressMessage("Usage", "DIMA001:Dependency Injection container is used directly")]
|
||||
public class Program
|
||||
{
|
||||
private static Lazy<string> gameInstallDir;
|
||||
private static readonly CircularBuffer<string> inputHistory = new(1000);
|
||||
private static int currentHistoryIndex;
|
||||
private static readonly CancellationTokenSource serverCts = new();
|
||||
|
||||
private static async Task Main(string[] args)
|
||||
{
|
||||
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolver.Handler;
|
||||
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += AssemblyResolver.Handler;
|
||||
|
||||
await StartServer(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize server here so that the JIT can compile the EntryPoint method without having to resolve dependencies
|
||||
/// that require the <see cref="AppDomain.AssemblyResolve" /> handler.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// https://stackoverflow.com/a/6089153/1277156
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static async Task StartServer(string[] args)
|
||||
{
|
||||
// The thread that writers to console is paused while selecting text in console. So console writer needs to be async.
|
||||
Log.Setup(true, isConsoleApp: !args.Contains("--embedded", StringComparer.OrdinalIgnoreCase));
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomainOnUnhandledException;
|
||||
PosixSignalRegistration.Create(PosixSignal.SIGTERM, CloseWindowHandler);
|
||||
PosixSignalRegistration.Create(PosixSignal.SIGQUIT, CloseWindowHandler);
|
||||
PosixSignalRegistration.Create(PosixSignal.SIGINT, CloseWindowHandler);
|
||||
PosixSignalRegistration.Create(PosixSignal.SIGHUP, CloseWindowHandler);
|
||||
|
||||
CultureManager.ConfigureCultureInfo();
|
||||
if (!Console.IsInputRedirected)
|
||||
{
|
||||
Console.TreatControlCAsInput = true;
|
||||
}
|
||||
|
||||
Log.Info($"Starting NitroxServer {NitroxEnvironment.ReleasePhase} v{NitroxEnvironment.Version} for {GameInfo.Subnautica.FullName}");
|
||||
Log.Debug($@"Process start args: ""{string.Join(@""", """, Environment.GetCommandLineArgs())}""");
|
||||
|
||||
Task handleConsoleInputTask;
|
||||
Server server;
|
||||
try
|
||||
{
|
||||
handleConsoleInputTask = HandleConsoleInputAsync(ConsoleCommandHandler(), serverCts.Token);
|
||||
AppMutex.Hold(() => Log.Info("Waiting on other Nitrox servers to initialize before starting.."), serverCts.Token);
|
||||
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
// Allow game path to be given as command argument
|
||||
string gameDir;
|
||||
if (args.Length > 0 && Directory.Exists(args[0]) && File.Exists(Path.Combine(args[0], GameInfo.Subnautica.ExeName)))
|
||||
{
|
||||
gameDir = Path.GetFullPath(args[0]);
|
||||
gameInstallDir = new Lazy<string>(() => gameDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
gameInstallDir = new Lazy<string>(() =>
|
||||
{
|
||||
return gameDir = NitroxUser.GamePath;
|
||||
});
|
||||
}
|
||||
Log.Info($"Using game files from: \'{gameInstallDir.Value}\'");
|
||||
|
||||
// TODO: Fix DI to not be slow (should not use IO in type constructors). Instead, use Lazy<T> (et al). This way, cancellation can be faster.
|
||||
NitroxServiceLocator.InitializeDependencyContainer(new SubnauticaServerAutoFacRegistrar());
|
||||
NitroxServiceLocator.BeginNewLifetimeScope();
|
||||
server = NitroxServiceLocator.LocateService<Server>();
|
||||
string serverSaveName = Server.GetSaveName(args, "My World");
|
||||
Log.SaveName = serverSaveName;
|
||||
|
||||
using (CancellationTokenSource portWaitCts = CancellationTokenSource.CreateLinkedTokenSource(serverCts.Token))
|
||||
{
|
||||
TimeSpan portWaitTimeout = TimeSpan.FromSeconds(30);
|
||||
portWaitCts.CancelAfter(portWaitTimeout);
|
||||
await WaitForAvailablePortAsync(server.Port, portWaitTimeout, portWaitCts.Token);
|
||||
}
|
||||
|
||||
if (!serverCts.IsCancellationRequested)
|
||||
{
|
||||
if (!server.Start(serverSaveName, serverCts))
|
||||
{
|
||||
throw new Exception("Unable to start server.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Info($"Server started ({Math.Round(watch.Elapsed.TotalSeconds, 1)}s)");
|
||||
Log.Info("To get help for commands, run help in console or /help in chatbox");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Allow other servers to start initializing.
|
||||
AppMutex.Release();
|
||||
}
|
||||
|
||||
await handleConsoleInputTask;
|
||||
server.Stop(true);
|
||||
|
||||
try
|
||||
{
|
||||
if (Environment.UserInteractive && Console.In != StreamReader.Null && Debugger.IsAttached)
|
||||
{
|
||||
Task.Delay(100).Wait(); // Wait for async logs to flush to console
|
||||
Console.WriteLine($"{Environment.NewLine}Press any key to continue . . .");
|
||||
Console.ReadKey(true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
Action<string> ConsoleCommandHandler()
|
||||
{
|
||||
ConsoleCommandProcessor commandProcessor = null;
|
||||
return submit =>
|
||||
{
|
||||
try
|
||||
{
|
||||
commandProcessor ??= NitroxServiceLocator.LocateService<ConsoleCommandProcessor>();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
commandProcessor?.ProcessCommand(submit, Optional.Empty, Perms.CONSOLE);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloseWindowHandler(PosixSignalContext context)
|
||||
{
|
||||
context.Cancel = false;
|
||||
serverCts?.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles per-key input of the console and passes input submit to <see cref="ConsoleCommandProcessor" />.
|
||||
/// </summary>
|
||||
private static async Task HandleConsoleInputAsync(Action<string> submitHandler, CancellationToken ct = default)
|
||||
{
|
||||
ConcurrentQueue<string> commandQueue = new();
|
||||
|
||||
if (Console.IsInputRedirected)
|
||||
{
|
||||
Log.Info("Server input stream is redirected");
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
string commandRead = Console.ReadLine();
|
||||
commandQueue.Enqueue(commandRead);
|
||||
}
|
||||
}, ct).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
Log.Error(t.Exception);
|
||||
}
|
||||
}, ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Info("Server input stream is available");
|
||||
StringBuilder inputLineBuilder = new();
|
||||
|
||||
void ClearInputLine()
|
||||
{
|
||||
currentHistoryIndex = 0;
|
||||
inputLineBuilder.Clear();
|
||||
Console.Write($"\r{new string(' ', Console.WindowWidth - 1)}\r");
|
||||
}
|
||||
|
||||
void RedrawInput(int start = 0, int end = 0)
|
||||
{
|
||||
int lastPosition = Console.CursorLeft;
|
||||
// Expand range to end if end value is -1
|
||||
if (start > -1 && end == -1)
|
||||
{
|
||||
end = Math.Max(inputLineBuilder.Length - start, 0);
|
||||
}
|
||||
|
||||
if (start == 0 && end == 0)
|
||||
{
|
||||
// Redraw entire line
|
||||
Console.Write($"\r{new string(' ', Console.WindowWidth - 1)}\r{inputLineBuilder}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Redraw part of line
|
||||
string changedInputSegment = inputLineBuilder.ToString(start, end);
|
||||
Console.CursorVisible = false;
|
||||
Console.Write($"{changedInputSegment}{new string(' ', inputLineBuilder.Length - changedInputSegment.Length - Console.CursorLeft + 1)}");
|
||||
Console.CursorVisible = true;
|
||||
}
|
||||
Console.CursorLeft = lastPosition;
|
||||
}
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
if (!Console.KeyAvailable)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(10, ct);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
|
||||
// Handle (ctrl) hotkeys
|
||||
if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0)
|
||||
{
|
||||
switch (keyInfo.Key)
|
||||
{
|
||||
case ConsoleKey.C:
|
||||
if (inputLineBuilder.Length > 0)
|
||||
{
|
||||
ClearInputLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
await serverCts.CancelAsync();
|
||||
return;
|
||||
case ConsoleKey.D:
|
||||
await serverCts.CancelAsync();
|
||||
return;
|
||||
default:
|
||||
// Unhandled modifier key
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (keyInfo.Modifiers == 0)
|
||||
{
|
||||
switch (keyInfo.Key)
|
||||
{
|
||||
case ConsoleKey.LeftArrow when Console.CursorLeft > 0:
|
||||
Console.CursorLeft--;
|
||||
continue;
|
||||
case ConsoleKey.RightArrow when Console.CursorLeft < inputLineBuilder.Length:
|
||||
Console.CursorLeft++;
|
||||
continue;
|
||||
case ConsoleKey.Backspace:
|
||||
if (inputLineBuilder.Length > Console.CursorLeft - 1 && Console.CursorLeft > 0)
|
||||
{
|
||||
inputLineBuilder.Remove(Console.CursorLeft - 1, 1);
|
||||
Console.CursorLeft--;
|
||||
Console.Write(' ');
|
||||
Console.CursorLeft--;
|
||||
RedrawInput();
|
||||
}
|
||||
continue;
|
||||
case ConsoleKey.Delete:
|
||||
if (inputLineBuilder.Length > 0 && Console.CursorLeft < inputLineBuilder.Length)
|
||||
{
|
||||
inputLineBuilder.Remove(Console.CursorLeft, 1);
|
||||
RedrawInput(Console.CursorLeft, inputLineBuilder.Length - Console.CursorLeft);
|
||||
}
|
||||
continue;
|
||||
case ConsoleKey.Home:
|
||||
Console.CursorLeft = 0;
|
||||
continue;
|
||||
case ConsoleKey.End:
|
||||
Console.CursorLeft = inputLineBuilder.Length;
|
||||
continue;
|
||||
case ConsoleKey.Escape:
|
||||
ClearInputLine();
|
||||
continue;
|
||||
case ConsoleKey.Tab:
|
||||
if (Console.CursorLeft + 4 < Console.WindowWidth)
|
||||
{
|
||||
inputLineBuilder.Insert(Console.CursorLeft, " ");
|
||||
RedrawInput(Console.CursorLeft, -1);
|
||||
Console.CursorLeft += 4;
|
||||
}
|
||||
continue;
|
||||
case ConsoleKey.UpArrow when inputHistory.Count > 0 && currentHistoryIndex > -inputHistory.Count:
|
||||
inputLineBuilder.Clear();
|
||||
inputLineBuilder.Append(inputHistory[--currentHistoryIndex]);
|
||||
RedrawInput();
|
||||
Console.CursorLeft = Math.Min(inputLineBuilder.Length, Console.WindowWidth);
|
||||
continue;
|
||||
case ConsoleKey.DownArrow when inputHistory.Count > 0 && currentHistoryIndex < 0:
|
||||
if (currentHistoryIndex == -1)
|
||||
{
|
||||
ClearInputLine();
|
||||
continue;
|
||||
}
|
||||
inputLineBuilder.Clear();
|
||||
inputLineBuilder.Append(inputHistory[++currentHistoryIndex]);
|
||||
RedrawInput();
|
||||
Console.CursorLeft = Math.Min(inputLineBuilder.Length, Console.WindowWidth);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Handle input submit to submit handler
|
||||
if (keyInfo.Key == ConsoleKey.Enter)
|
||||
{
|
||||
string submit = inputLineBuilder.ToString();
|
||||
if (inputHistory.Count == 0 || inputHistory[inputHistory.LastChangedIndex] != submit)
|
||||
{
|
||||
inputHistory.Add(submit);
|
||||
}
|
||||
currentHistoryIndex = 0;
|
||||
commandQueue.Enqueue(submit);
|
||||
inputLineBuilder.Clear();
|
||||
Console.WriteLine();
|
||||
continue;
|
||||
}
|
||||
|
||||
// If unhandled key, append as input.
|
||||
if (keyInfo.KeyChar != 0)
|
||||
{
|
||||
Console.Write(keyInfo.KeyChar);
|
||||
if (Console.CursorLeft - 1 < inputLineBuilder.Length)
|
||||
{
|
||||
inputLineBuilder.Insert(Console.CursorLeft - 1, keyInfo.KeyChar);
|
||||
RedrawInput(Console.CursorLeft, -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
inputLineBuilder.Append(keyInfo.KeyChar);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, ct).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
Log.Error(t.Exception);
|
||||
}
|
||||
}, ct);
|
||||
}
|
||||
|
||||
using IpcHost ipcHost = IpcHost.StartReadingCommands(command => commandQueue.Enqueue(command), ct);
|
||||
|
||||
if (!Console.IsInputRedirected)
|
||||
{
|
||||
// Important to not hang process: keep command handler on the main thread when input not redirected (i.e. don't Task.Run)
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
while (commandQueue.TryDequeue(out string command))
|
||||
{
|
||||
submitHandler(command);
|
||||
}
|
||||
try
|
||||
{
|
||||
await Task.Delay(10, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Important to not hang process (when running launcher from release exe): free main thread if input redirected
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
while (commandQueue.TryDequeue(out string command))
|
||||
{
|
||||
submitHandler(command);
|
||||
}
|
||||
try
|
||||
{
|
||||
await Task.Delay(10, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}, ct).ContinueWithHandleError();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WaitForAvailablePortAsync(int port, TimeSpan timeout = default, CancellationToken ct = default)
|
||||
{
|
||||
if (timeout == default)
|
||||
{
|
||||
timeout = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
else
|
||||
{
|
||||
Validate.IsTrue(timeout.TotalSeconds >= 5, "Timeout must be at least 5 seconds.");
|
||||
}
|
||||
|
||||
int messageLength = 0;
|
||||
void PrintPortWarn(TimeSpan timeRemaining)
|
||||
{
|
||||
string message = $"Port {port} UDP is already in use. Please change the server port or close out any program that may be using it. Retrying for {Math.Floor(timeRemaining.TotalSeconds)} seconds until it is available...";
|
||||
messageLength = message.Length;
|
||||
Log.Warn(message);
|
||||
}
|
||||
|
||||
DateTimeOffset time = DateTimeOffset.UtcNow;
|
||||
bool first = true;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
IPEndPoint endPoint = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners().FirstOrDefault(ip => ip.Port == port);
|
||||
if (endPoint == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
PrintPortWarn(timeout);
|
||||
}
|
||||
else if (Environment.UserInteractive && !Console.IsInputRedirected && Console.In != StreamReader.Null)
|
||||
{
|
||||
// If not first time, move cursor up the number of lines it takes up to overwrite previous message
|
||||
int numberOfLines = (int)Math.Ceiling( ((double)messageLength + 15) / Console.BufferWidth );
|
||||
for (int i = 0; i < numberOfLines; i++)
|
||||
{
|
||||
if (Console.CursorTop > 0) // Check to ensure we don't go out of bounds
|
||||
{
|
||||
Console.CursorTop--;
|
||||
}
|
||||
}
|
||||
Console.CursorLeft = 0;
|
||||
|
||||
PrintPortWarn(timeout - (DateTimeOffset.UtcNow - time));
|
||||
}
|
||||
|
||||
await Task.Delay(500, ct);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private static void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
if (e.ExceptionObject is Exception ex)
|
||||
{
|
||||
Log.Error(ex);
|
||||
}
|
||||
if (!Environment.UserInteractive || Console.IsInputRedirected || Console.In == StreamReader.Null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement log file opening by server name
|
||||
/*string mostRecentLogFile = Log.GetMostRecentLogFile(); // Log.SaveName
|
||||
if (mostRecentLogFile == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Info("Press L to open log file before closing. Press any other key to close . . .");*/
|
||||
Log.Info("Press L to open log folder before closing. Press any other key to close . . .");
|
||||
ConsoleKeyInfo key = Console.ReadKey(true);
|
||||
|
||||
if (key.Key == ConsoleKey.L)
|
||||
{
|
||||
// Log.Info($"Opening log file at: {mostRecentLogFile}..");
|
||||
// using Process process = FileSystem.Instance.OpenOrExecuteFile(mostRecentLogFile);
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = Log.LogDirectory,
|
||||
Verb = "open",
|
||||
UseShellExecute = true
|
||||
})?.Dispose();
|
||||
}
|
||||
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
private static class AssemblyResolver
|
||||
{
|
||||
private static string currentExecutableDirectory;
|
||||
private static readonly Dictionary<string, Assembly> resolvedAssemblyCache = [];
|
||||
|
||||
public static Assembly Handler(object sender, ResolveEventArgs args)
|
||||
{
|
||||
static Assembly ResolveFromLib(ReadOnlySpan<char> dllName)
|
||||
{
|
||||
dllName = dllName.Slice(0, Math.Max(dllName.IndexOf(','), 0));
|
||||
if (dllName.IsEmpty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (!dllName.EndsWith(".dll"))
|
||||
{
|
||||
dllName = string.Concat(dllName, ".dll");
|
||||
}
|
||||
if (dllName.EndsWith(".resources.dll"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string dllNameStr = dllName.ToString();
|
||||
// If available, return cached assembly
|
||||
if (resolvedAssemblyCache.TryGetValue(dllNameStr, out Assembly val))
|
||||
{
|
||||
return val;
|
||||
}
|
||||
|
||||
// Load DLLs where this program (exe) is located
|
||||
string dllPath = Path.Combine(GetExecutableDirectory(), "lib", dllNameStr);
|
||||
// Prefer to use Newtonsoft dll from game instead of our own due to protobuf issues. TODO: Remove when we do our own deserialization of game data instead of using the game's protobuf.
|
||||
if (dllPath.IndexOf("Newtonsoft.Json.dll", StringComparison.OrdinalIgnoreCase) >= 0 || !File.Exists(dllPath))
|
||||
{
|
||||
// Try find game managed libraries
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
dllPath = Path.Combine(gameInstallDir.Value, "Resources", "Data", "Managed", dllNameStr);
|
||||
}
|
||||
else
|
||||
{
|
||||
dllPath = Path.Combine(gameInstallDir.Value, "Subnautica_Data", "Managed", dllNameStr);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read assemblies as bytes as to not lock the file so that Nitrox can patch assemblies while server is running.
|
||||
Assembly assembly = Assembly.Load(File.ReadAllBytes(dllPath));
|
||||
return resolvedAssemblyCache[dllNameStr] = assembly;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Assembly assembly = ResolveFromLib(args.Name);
|
||||
if (assembly == null && !args.Name.Contains(".resources"))
|
||||
{
|
||||
assembly = Assembly.Load(args.Name);
|
||||
}
|
||||
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private static string GetExecutableDirectory()
|
||||
{
|
||||
if (currentExecutableDirectory != null)
|
||||
{
|
||||
return currentExecutableDirectory;
|
||||
}
|
||||
string pathAttempt = Assembly.GetEntryAssembly()?.Location;
|
||||
if (string.IsNullOrWhiteSpace(pathAttempt))
|
||||
{
|
||||
using Process proc = Process.GetCurrentProcess();
|
||||
pathAttempt = proc.MainModule?.FileName;
|
||||
}
|
||||
return currentExecutableDirectory = new Uri(Path.GetDirectoryName(pathAttempt ?? ".") ?? Directory.GetCurrentDirectory()).LocalPath;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user