Browse Source

macos update compat fixes

pull/438/head
ionite34 11 months ago
parent
commit
9fe0ef8689
No known key found for this signature in database
GPG Key ID: B3404C5F3827849B
  1. 55
      StabilityMatrix.Avalonia/Program.cs
  2. 20
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  3. 6
      StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs
  4. 49
      StabilityMatrix.Core/Helper/Compat.cs
  5. 13
      StabilityMatrix.Core/Models/FileInterfaces/DirectoryPath.cs
  6. 30
      StabilityMatrix.Core/Processes/ProcessRunner.cs
  7. 39
      StabilityMatrix.Core/Updater/UpdateHelper.cs

55
StabilityMatrix.Avalonia/Program.cs

@ -25,6 +25,7 @@ using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Updater; using StabilityMatrix.Core.Updater;
namespace StabilityMatrix.Avalonia; namespace StabilityMatrix.Avalonia;
@ -55,8 +56,7 @@ public static class Program
SetDebugBuild(); SetDebugBuild();
var parseResult = Parser var parseResult = Parser
.Default .Default.ParseArguments<AppArgs>(args)
.ParseArguments<AppArgs>(args)
.WithNotParsed(errors => .WithNotParsed(errors =>
{ {
foreach (var error in errors) foreach (var error in errors)
@ -147,7 +147,10 @@ public static class Program
if (Args.UseOpenGlRendering) if (Args.UseOpenGlRendering)
{ {
app = app.With( app = app.With(
new Win32PlatformOptions { RenderingMode = [Win32RenderingMode.Wgl, Win32RenderingMode.Software] } new Win32PlatformOptions
{
RenderingMode = [Win32RenderingMode.Wgl, Win32RenderingMode.Software]
}
); );
} }
@ -156,7 +159,10 @@ public static class Program
app = app.With(new Win32PlatformOptions { RenderingMode = new[] { Win32RenderingMode.Software } }) app = app.With(new Win32PlatformOptions { RenderingMode = new[] { Win32RenderingMode.Software } })
.With(new X11PlatformOptions { RenderingMode = new[] { X11RenderingMode.Software } }) .With(new X11PlatformOptions { RenderingMode = new[] { X11RenderingMode.Software } })
.With( .With(
new AvaloniaNativePlatformOptions { RenderingMode = new[] { AvaloniaNativeRenderingMode.Software } } new AvaloniaNativePlatformOptions
{
RenderingMode = new[] { AvaloniaNativeRenderingMode.Software }
}
); );
} }
@ -173,8 +179,6 @@ public static class Program
return; return;
// Copy our current file to the parent directory, overwriting the old app file // Copy our current file to the parent directory, overwriting the old app file
var currentExe = Compat.AppCurrentDir.JoinFile(Compat.GetExecutableName());
var targetExe = parentDir.JoinFile(Compat.GetExecutableName());
var isCopied = false; var isCopied = false;
@ -188,7 +192,27 @@ public static class Program
{ {
try try
{ {
currentExe.CopyTo(targetExe, true); if (Compat.IsMacOS)
{
var currentApp = Compat.AppBundleCurrentPath!;
var targetApp = parentDir.JoinDir(Compat.GetAppName());
// Since macOS has issues with signature caching, delete the target app first
if (targetApp.Exists)
{
targetApp.Delete(true);
}
currentApp.CopyTo(targetApp);
}
else
{
var currentExe = Compat.AppCurrentPath;
var targetExe = parentDir.JoinFile(Compat.GetExecutableName());
currentExe.CopyTo(targetExe, true);
}
isCopied = true; isCopied = true;
break; break;
} }
@ -204,11 +228,13 @@ public static class Program
Environment.Exit(1); Environment.Exit(1);
} }
var targetAppOrBundle = Path.Combine(parentDir, Compat.GetAppName());
// Ensure permissions are set for unix // Ensure permissions are set for unix
if (Compat.IsUnix) if (Compat.IsUnix)
{ {
File.SetUnixFileMode( File.SetUnixFileMode(
targetExe, // 0755 targetAppOrBundle, // 0755
UnixFileMode.UserRead UnixFileMode.UserRead
| UnixFileMode.UserWrite | UnixFileMode.UserWrite
| UnixFileMode.UserExecute | UnixFileMode.UserExecute
@ -220,7 +246,10 @@ public static class Program
} }
// Start the new app while passing our own PID to wait for exit // Start the new app while passing our own PID to wait for exit
Process.Start(targetExe, $"--wait-for-exit-pid {Environment.ProcessId}"); ProcessRunner.StartApp(
targetAppOrBundle,
new[] { "--wait-for-exit-pid", $"{Environment.ProcessId}" }
);
// Shutdown the current app // Shutdown the current app
Environment.Exit(0); Environment.Exit(0);
@ -274,7 +303,8 @@ public static class Program
{ {
SentrySdk.Init(o => SentrySdk.Init(o =>
{ {
o.Dsn = "https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328"; o.Dsn =
"https://eac7a5ea065d44cf9a8565e0f1817da2@o4505314753380352.ingest.sentry.io/4505314756067328";
o.StackTraceMode = StackTraceMode.Enhanced; o.StackTraceMode = StackTraceMode.Enhanced;
o.TracesSampleRate = 1.0; o.TracesSampleRate = 1.0;
o.IsGlobalModeEnabled = true; o.IsGlobalModeEnabled = true;
@ -301,7 +331,10 @@ public static class Program
}); });
} }
private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) private static void TaskScheduler_UnobservedTaskException(
object? sender,
UnobservedTaskExceptionEventArgs e
)
{ {
if (e.Exception is Exception ex) if (e.Exception is Exception ex)
{ {

20
StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs

@ -18,6 +18,7 @@ using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater; using StabilityMatrix.Core.Updater;
@ -156,7 +157,7 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
if (Compat.IsUnix) if (Compat.IsUnix)
{ {
File.SetUnixFileMode( File.SetUnixFileMode(
UpdateHelper.ExecutablePath, // 0755 UpdateHelper.ExecutablePath.FullPath, // 0755
UnixFileMode.UserRead UnixFileMode.UserRead
| UnixFileMode.UserWrite | UnixFileMode.UserWrite
| UnixFileMode.UserExecute | UnixFileMode.UserExecute
@ -170,7 +171,13 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
UpdateText = "Getting a few things ready..."; UpdateText = "Getting a few things ready...";
await using (new MinimumDelay(500, 1000)) await using (new MinimumDelay(500, 1000))
{ {
Process.Start(UpdateHelper.ExecutablePath, $"--wait-for-exit-pid {Environment.ProcessId}"); await Task.Run(() =>
{
ProcessRunner.StartApp(
UpdateHelper.ExecutablePath.FullPath,
new[] { "--wait-for-exit-pid", $"{Environment.ProcessId}" }
);
});
} }
UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds..."; UpdateText = "Update complete. Restarting Stability Matrix in 3 seconds...";
@ -259,7 +266,10 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
var currentVersionBlock = results.FindIndex(x => x.Version == currentVersion.WithoutMetadata()); var currentVersionBlock = results.FindIndex(x => x.Version == currentVersion.WithoutMetadata());
// For mismatching build metadata, add one // For mismatching build metadata, add one
if (currentVersionBlock != -1 && results[currentVersionBlock].Version?.Metadata != currentVersion.Metadata) if (
currentVersionBlock != -1
&& results[currentVersionBlock].Version?.Metadata != currentVersion.Metadata
)
{ {
currentVersionBlock++; currentVersionBlock++;
} }
@ -267,7 +277,9 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
// Support for previous pre-release without changelogs // Support for previous pre-release without changelogs
if (currentVersionBlock == -1) if (currentVersionBlock == -1)
{ {
currentVersionBlock = results.FindIndex(x => x.Version == currentVersion.WithoutPrereleaseOrMetadata()); currentVersionBlock = results.FindIndex(
x => x.Version == currentVersion.WithoutPrereleaseOrMetadata()
);
// Add 1 if found to include the current release // Add 1 if found to include the current release
if (currentVersionBlock != -1) if (currentVersionBlock != -1)

6
StabilityMatrix.Avalonia/ViewModels/Settings/MainSettingsViewModel.cs

@ -51,6 +51,7 @@ using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Settings; using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
@ -302,7 +303,10 @@ public partial class MainSettingsViewModel : PageViewModelBase
{ {
if (await dialog.ShowAsync() == ContentDialogResult.Primary) if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{ {
Process.Start(Compat.AppCurrentPath); // Start the new app while passing our own PID to wait for exit
Process.Start(Compat.AppCurrentPath, $"--wait-for-exit-pid {Environment.ProcessId}");
// Shutdown the current app
App.Shutdown(); App.Shutdown();
} }
}); });

49
StabilityMatrix.Core/Helper/Compat.cs

@ -4,6 +4,7 @@ using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using Semver; using Semver;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Helper;
@ -62,10 +63,21 @@ public static class Compat
public static DirectoryPath AppCurrentDir { get; } public static DirectoryPath AppCurrentDir { get; }
/// <summary> /// <summary>
/// Current path to the app. /// Current path to the app binary.
/// </summary> /// </summary>
public static FilePath AppCurrentPath => AppCurrentDir.JoinFile(GetExecutableName()); public static FilePath AppCurrentPath => AppCurrentDir.JoinFile(GetExecutableName());
/// <summary>
/// Path to the .app bundle on macOS.
/// </summary>
[SupportedOSPlatform("macos")]
public static DirectoryPath? AppBundleCurrentPath { get; }
/// <summary>
/// Either the <see cref="AppCurrentPath"/> File or <see cref="AppBundleCurrentPath"/> directory on macOS.
/// </summary>
public static FileSystemPath AppOrBundleCurrentPath => IsMacOS ? AppBundleCurrentPath! : AppCurrentPath;
// File extensions // File extensions
/// <summary> /// <summary>
/// Platform-specific executable extension. /// Platform-specific executable extension.
@ -103,7 +115,14 @@ public static class Compat
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{ {
Platform = PlatformKind.MacOS | PlatformKind.Unix; Platform = PlatformKind.MacOS | PlatformKind.Unix;
AppCurrentDir = AppContext.BaseDirectory; // TODO: check this
// This is ./<AppName>.app/Contents/MacOS
var macDir = new DirectoryPath(AppContext.BaseDirectory);
// We need to go up two directories to get the .app directory
AppBundleCurrentPath = macDir.Parent?.Parent;
// Then CurrentDir is the next parent
AppCurrentDir = AppBundleCurrentPath!.Parent!;
ExeExtension = ""; ExeExtension = "";
DllExtension = ".dylib"; DllExtension = ".dylib";
} }
@ -112,11 +131,9 @@ public static class Compat
Platform = PlatformKind.Linux | PlatformKind.Unix; Platform = PlatformKind.Linux | PlatformKind.Unix;
// For AppImage builds, the path is in `$APPIMAGE` // For AppImage builds, the path is in `$APPIMAGE`
var appPath = var appPath = Environment.GetEnvironmentVariable("APPIMAGE") ?? AppContext.BaseDirectory;
Environment.GetEnvironmentVariable("APPIMAGE") ?? AppContext.BaseDirectory;
AppCurrentDir = AppCurrentDir =
Path.GetDirectoryName(appPath) Path.GetDirectoryName(appPath) ?? throw new Exception("Could not find application directory");
?? throw new Exception("Could not find application directory");
ExeExtension = ""; ExeExtension = "";
DllExtension = ".so"; DllExtension = ".so";
} }
@ -186,12 +203,24 @@ public static class Compat
return Path.GetFileName(fullPath); return Path.GetFileName(fullPath);
} }
/// <summary>
/// Get the current application executable or bundle name.
/// </summary>
public static string GetAppName()
{
// For other platforms, this is the same as the executable name
if (!IsMacOS)
{
return GetExecutableName();
}
// On macOS, get name of current bundle
return Path.GetFileName(AppBundleCurrentPath.Unwrap());
}
public static string GetEnvPathWithExtensions(params string[] paths) public static string GetEnvPathWithExtensions(params string[] paths)
{ {
var currentPath = Environment.GetEnvironmentVariable( var currentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process);
"PATH",
EnvironmentVariableTarget.Process
);
var newPath = string.Join(PathDelimiter, paths); var newPath = string.Join(PathDelimiter, paths);
if (string.IsNullOrEmpty(currentPath)) if (string.IsNullOrEmpty(currentPath))

13
StabilityMatrix.Core/Models/FileInterfaces/DirectoryPath.cs

@ -249,6 +249,19 @@ public class DirectoryPath : FileSystemPath, IPathObject, IEnumerable<FileSystem
Info.EnumerateDirectories(searchPattern, searchOption) Info.EnumerateDirectories(searchPattern, searchOption)
.Select(directory => new DirectoryPath(directory)); .Select(directory => new DirectoryPath(directory));
/// <summary>
/// Return a new <see cref="DirectoryPath"/> with the given file name.
/// </summary>
public DirectoryPath WithName(string directoryName)
{
if (Path.GetDirectoryName(FullPath) is { } directory && !string.IsNullOrWhiteSpace(directory))
{
return new DirectoryPath(directory, directoryName);
}
return new DirectoryPath(directoryName);
}
public override string ToString() => FullPath; public override string ToString() => FullPath;
/// <inheritdoc /> /// <inheritdoc />

30
StabilityMatrix.Core/Processes/ProcessRunner.cs

@ -29,6 +29,27 @@ public static class ProcessRunner
OpenUrl(url.AbsoluteUri); OpenUrl(url.AbsoluteUri);
} }
/// <summary>
/// Start an executable or .app on macOS.
/// </summary>
public static Process StartApp(string path, ProcessArgs args)
{
if (Compat.IsMacOS)
{
var startInfo = new ProcessStartInfo
{
FileName = "open",
Arguments = args.Prepend([path, "--args"]),
UseShellExecute = true
};
return Process.Start(startInfo)
?? throw new NullReferenceException("Process.Start returned null");
}
return Process.Start(args.Prepend(path));
}
/// <summary> /// <summary>
/// Opens the given folder in the system file explorer. /// Opens the given folder in the system file explorer.
/// </summary> /// </summary>
@ -372,7 +393,10 @@ public static class ProcessRunner
}; };
} }
public static Task<ProcessResult> RunBashCommand(IEnumerable<string> commands, string workingDirectory = "") public static Task<ProcessResult> RunBashCommand(
IEnumerable<string> commands,
string workingDirectory = ""
)
{ {
// Quote arguments containing spaces // Quote arguments containing spaces
var args = string.Join(" ", commands.Select(Quote)); var args = string.Join(" ", commands.Select(Quote));
@ -421,7 +445,9 @@ public static class ProcessRunner
catch (SystemException) { } catch (SystemException) { }
throw new ProcessException( throw new ProcessException(
"Process " + (processName == null ? "" : processName + " ") + $"failed with exit-code {process.ExitCode}." "Process "
+ (processName == null ? "" : processName + " ")
+ $"failed with exit-code {process.ExitCode}."
); );
} }
} }

39
StabilityMatrix.Core/Updater/UpdateHelper.cs

@ -32,7 +32,10 @@ public class UpdateHelper : IUpdateHelper
public const string UpdateFolderName = ".StabilityMatrixUpdate"; public const string UpdateFolderName = ".StabilityMatrixUpdate";
public static DirectoryPath UpdateFolder => Compat.AppCurrentDir.JoinDir(UpdateFolderName); public static DirectoryPath UpdateFolder => Compat.AppCurrentDir.JoinDir(UpdateFolderName);
public static FilePath ExecutablePath => UpdateFolder.JoinFile(Compat.GetExecutableName()); public static IPathObject ExecutablePath =>
Compat.IsMacOS
? UpdateFolder.JoinDir(Compat.GetAppName())
: UpdateFolder.JoinFile(Compat.GetAppName());
/// <inheritdoc /> /// <inheritdoc />
public event EventHandler<UpdateStatusChangedEventArgs>? UpdateStatusChanged; public event EventHandler<UpdateStatusChangedEventArgs>? UpdateStatusChanged;
@ -123,7 +126,7 @@ public class UpdateHelper : IUpdateHelper
.EnumerateFiles("*.*", SearchOption.AllDirectories) .EnumerateFiles("*.*", SearchOption.AllDirectories)
.First(f => f.Extension.ToLowerInvariant() is ".exe" or ".appimage"); .First(f => f.Extension.ToLowerInvariant() is ".exe" or ".appimage");
await binaryFile.MoveToAsync(ExecutablePath).ConfigureAwait(false); await binaryFile.MoveToAsync((FilePath)ExecutablePath).ConfigureAwait(false);
} }
else if (downloadFile.Extension == ".dmg") else if (downloadFile.Extension == ".dmg")
{ {
@ -139,9 +142,10 @@ public class UpdateHelper : IUpdateHelper
// Extract dmg contents // Extract dmg contents
await ArchiveHelper.ExtractDmg(downloadFile, extractDir).ConfigureAwait(false); await ArchiveHelper.ExtractDmg(downloadFile, extractDir).ConfigureAwait(false);
// Find app and move it up to the root // Find app dir and move it up to the root
var appFile = extractDir.EnumerateFiles("*.app").First(); var appBundle = extractDir.EnumerateDirectories("*.app").First();
await appFile.MoveToAsync(ExecutablePath).ConfigureAwait(false);
await appBundle.MoveToAsync((DirectoryPath)ExecutablePath).ConfigureAwait(false);
} }
// Otherwise just rename // Otherwise just rename
else else
@ -210,10 +214,10 @@ public class UpdateHelper : IUpdateHelper
new UpdateStatusChangedEventArgs new UpdateStatusChangedEventArgs
{ {
LatestUpdate = update, LatestUpdate = update,
UpdateChannels = updateManifest.Updates.ToDictionary( UpdateChannels = updateManifest
kv => kv.Key, .Updates.Select(kv => (kv.Key, kv.Value.GetInfoForCurrentPlatform()))
kv => kv.Value.GetInfoForCurrentPlatform() .Where(kv => kv.Item2 is not null)
)! .ToDictionary(kv => kv.Item1, kv => kv.Item2)!
} }
); );
return; return;
@ -222,15 +226,14 @@ public class UpdateHelper : IUpdateHelper
logger.LogInformation("No update available"); logger.LogInformation("No update available");
OnUpdateStatusChanged( var args = new UpdateStatusChangedEventArgs
new UpdateStatusChangedEventArgs {
{ UpdateChannels = updateManifest
UpdateChannels = updateManifest.Updates.ToDictionary( .Updates.Select(kv => (kv.Key, kv.Value.GetInfoForCurrentPlatform()))
kv => kv.Key, .Where(kv => kv.Item2 is not null)
kv => kv.Value.GetInfoForCurrentPlatform() .ToDictionary(kv => kv.Item1, kv => kv.Item2)!
)! };
} OnUpdateStatusChanged(args);
);
} }
catch (Exception e) catch (Exception e)
{ {

Loading…
Cancel
Save