mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 02:42:54 +08:00
1e3c332658
Closes https://github.com/ppy/osu/issues/26510. Time for a rant. Technically, this "broke" with9e8d07d314
, but it is actually an end result of upstream behaviours that I am failing to find a better description for than "utterly broken". Squirrel (the installer we use) has unit tests. Which is great, power to them. However, the method in which that testing is implemented leads to epic levels of WTF breakage. To determine whether Squirrel is being tested right now, it is checking all currently loaded assemblies, and determining that if any loaded assembly contains the magic string of "NUNIT" - among others - it must be being tested right now:2442721748/src/Squirrel/SimpleSplat/PlatformModeDetector.cs (L17-L32)
If one assumes that there is no conceivable way that an NUnit assembly *may* be loaded *without* it being a test context, this *may* seem sane. Foreshadowing. (Now, to avoid being hypocritical, we also do this, *but* we do this by checking if the *entry* assembly is an NUnit:92db55a527/osu.Framework/Development/DebugUtils.cs (L16-L34)
which seems *much* saner, no?) Now, why did this break with9e8d07d314
*specifically*, you might wonder? Well the reason is this line:3d3f58c252/osu.Desktop/NVAPI.cs (L183)
Yes you are reading this correctly, it's not NVAPI anything itself that breaks this, it is *a log statement*. To be precise, what the log statement *does* to provoke this, is calling into framework. That causes the framework assembly to load, *which* transitively loads the `nunit.framework` assembly. (If you ever find yourself wanting to find out this sort of cursed knowledge - I hope you never need to - you can run something along the lines of dotnet-trace collect --providers Microsoft-Windows-DotNETRuntime:4 -- .\osu!.exe then open the resulting trace in PerfView, and then search the `Microsoft-Windows-DotNETRuntime/AssemblyLoader/Start` log for the cursed assembly. In this case, the relevant entry said something along the lines of HasStack="True" ThreadID="23,924" ProcessorNumber="0" ClrInstanceID="6" AssemblyName="nunit.framework, Version=3.13.3.0, Culture=neutral, PublicKeyToken=2638cd05610744eb" AssemblyPath="" RequestingAssembly="osu.Framework, Version=2024.113.0.0, Culture=neutral, PublicKeyToken=null" AssemblyLoadContext="Default" RequestingAssemblyLoadContext="Default" ActivityID="/#21032/1/26/" Either that or just comment the log line for kicks. But the above is *much* faster.) Now, what *happens* if Squirrel "detects" that it is being "tested"? Well, it will refuse to close after executing the "hooks" defined via `SquirrelAwareApp`:2442721748/src/Squirrel/SquirrelAwareApp.cs (L85-L88)
and it will also refuse to create version shortcuts:2442721748/src/Squirrel/UpdateManager.Shortcuts.cs (L63-L65)
Sounds familiar, don't it? There are days on which I tire of computers. Today is one of them.
195 lines
8.2 KiB
C#
195 lines
8.2 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Runtime.Versioning;
|
|
using osu.Desktop.LegacyIpc;
|
|
using osu.Framework;
|
|
using osu.Framework.Development;
|
|
using osu.Framework.Logging;
|
|
using osu.Framework.Platform;
|
|
using osu.Game;
|
|
using osu.Game.IPC;
|
|
using osu.Game.Tournament;
|
|
using SDL2;
|
|
using Squirrel;
|
|
|
|
namespace osu.Desktop
|
|
{
|
|
public static class Program
|
|
{
|
|
#if DEBUG
|
|
private const string base_game_name = @"osu-development";
|
|
#else
|
|
private const string base_game_name = @"osu";
|
|
#endif
|
|
|
|
private static LegacyTcpIpcProvider? legacyIpc;
|
|
|
|
[STAThread]
|
|
public static void Main(string[] args)
|
|
{
|
|
/*
|
|
* WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
|
|
*
|
|
* Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it.
|
|
* To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit,
|
|
* namely by checking loaded assemblies:
|
|
* https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32
|
|
*
|
|
* If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded -
|
|
* the app will then do completely broken things like:
|
|
* - not creating system shortcuts (as the logic is if'd out if "running tests")
|
|
* - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests")
|
|
*/
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
var windowsVersion = Environment.OSVersion.Version;
|
|
|
|
// While .NET 6 still supports Windows 7 and above, we are limited by realm currently, as they choose to only support 8.1 and higher.
|
|
// See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
|
|
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
|
|
{
|
|
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
|
|
// disabling it ourselves.
|
|
// We could also better detect compatibility mode if required:
|
|
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
|
|
SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
|
|
"Your operating system is too old to run osu!",
|
|
"This version of osu! requires at least Windows 8.1 to run.\n"
|
|
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n"
|
|
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero);
|
|
return;
|
|
}
|
|
|
|
setupSquirrel();
|
|
}
|
|
|
|
// NVIDIA profiles are based on the executable name of a process.
|
|
// Lazer and stable share the same executable name.
|
|
// Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup.
|
|
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
|
|
|
// Back up the cwd before DesktopGameHost changes it
|
|
string cwd = Environment.CurrentDirectory;
|
|
|
|
string gameName = base_game_name;
|
|
bool tournamentClient = false;
|
|
|
|
foreach (string arg in args)
|
|
{
|
|
string[] split = arg.Split('=');
|
|
|
|
string key = split[0];
|
|
string val = split.Length > 1 ? split[1] : string.Empty;
|
|
|
|
switch (key)
|
|
{
|
|
case "--tournament":
|
|
tournamentClient = true;
|
|
break;
|
|
|
|
case "--debug-client-id":
|
|
if (!DebugUtils.IsDebugBuild)
|
|
throw new InvalidOperationException("Cannot use this argument in a non-debug build.");
|
|
|
|
if (!int.TryParse(val, out int clientID))
|
|
throw new ArgumentException("Provided client ID must be an integer.");
|
|
|
|
gameName = $"{base_game_name}-{clientID}";
|
|
break;
|
|
}
|
|
}
|
|
|
|
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = !tournamentClient }))
|
|
{
|
|
if (!host.IsPrimaryInstance)
|
|
{
|
|
if (trySendIPCMessage(host, cwd, args))
|
|
return;
|
|
|
|
// we want to allow multiple instances to be started when in debug.
|
|
if (!DebugUtils.IsDebugBuild)
|
|
{
|
|
Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (host.IsPrimaryInstance)
|
|
{
|
|
try
|
|
{
|
|
Logger.Log("Starting legacy IPC provider...");
|
|
legacyIpc = new LegacyTcpIpcProvider();
|
|
legacyIpc.Bind();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Error(ex, "Failed to start legacy IPC provider");
|
|
}
|
|
}
|
|
|
|
if (tournamentClient)
|
|
host.Run(new TournamentGame());
|
|
else
|
|
host.Run(new OsuGameDesktop(args));
|
|
}
|
|
}
|
|
|
|
private static bool trySendIPCMessage(IIpcHost host, string cwd, string[] args)
|
|
{
|
|
if (args.Length == 1 && args[0].StartsWith(OsuGameBase.OSU_PROTOCOL, StringComparison.Ordinal))
|
|
{
|
|
var osuSchemeLinkHandler = new OsuSchemeLinkIPCChannel(host);
|
|
if (!osuSchemeLinkHandler.HandleLinkAsync(args[0]).Wait(3000))
|
|
throw new IPCTimeoutException(osuSchemeLinkHandler.GetType());
|
|
|
|
return true;
|
|
}
|
|
|
|
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
|
|
{
|
|
var importer = new ArchiveImportIPCChannel(host);
|
|
|
|
foreach (string file in args)
|
|
{
|
|
Console.WriteLine(@"Importing {0}", file);
|
|
if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000))
|
|
throw new IPCTimeoutException(importer.GetType());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
private static void setupSquirrel()
|
|
{
|
|
SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) =>
|
|
{
|
|
tools.CreateShortcutForThisExe();
|
|
tools.CreateUninstallerRegistryEntry();
|
|
}, onAppUpdate: (_, tools) =>
|
|
{
|
|
tools.CreateUninstallerRegistryEntry();
|
|
}, onAppUninstall: (_, tools) =>
|
|
{
|
|
tools.RemoveShortcutForThisExe();
|
|
tools.RemoveUninstallerRegistryEntry();
|
|
}, onEveryRun: (_, _, _) =>
|
|
{
|
|
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
|
|
// causes the right-click context menu to function incorrectly.
|
|
//
|
|
// This may turn out to be non-required after an alternative solution is implemented.
|
|
// see https://github.com/clowd/Clowd.Squirrel/issues/24
|
|
// tools.SetProcessAppUserModelId();
|
|
});
|
|
}
|
|
}
|
|
}
|