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. 53
      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. 37
      StabilityMatrix.Core/Updater/UpdateHelper.cs

53
StabilityMatrix.Avalonia/Program.cs

@ -25,6 +25,7 @@ using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Updater;
namespace StabilityMatrix.Avalonia;
@ -55,8 +56,7 @@ public static class Program
SetDebugBuild();
var parseResult = Parser
.Default
.ParseArguments<AppArgs>(args)
.Default.ParseArguments<AppArgs>(args)
.WithNotParsed(errors =>
{
foreach (var error in errors)
@ -147,7 +147,10 @@ public static class Program
if (Args.UseOpenGlRendering)
{
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 } })
.With(new X11PlatformOptions { RenderingMode = new[] { X11RenderingMode.Software } })
.With(
new AvaloniaNativePlatformOptions { RenderingMode = new[] { AvaloniaNativeRenderingMode.Software } }
new AvaloniaNativePlatformOptions
{
RenderingMode = new[] { AvaloniaNativeRenderingMode.Software }
}
);
}
@ -173,8 +179,6 @@ public static class Program
return;
// 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;
@ -188,7 +192,27 @@ public static class Program
{
try
{
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;
break;
}
@ -204,11 +228,13 @@ public static class Program
Environment.Exit(1);
}
var targetAppOrBundle = Path.Combine(parentDir, Compat.GetAppName());
// Ensure permissions are set for unix
if (Compat.IsUnix)
{
File.SetUnixFileMode(
targetExe, // 0755
targetAppOrBundle, // 0755
UnixFileMode.UserRead
| UnixFileMode.UserWrite
| UnixFileMode.UserExecute
@ -220,7 +246,10 @@ public static class Program
}
// 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
Environment.Exit(0);
@ -274,7 +303,8 @@ public static class Program
{
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.TracesSampleRate = 1.0;
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)
{

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

@ -18,6 +18,7 @@ using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
@ -156,7 +157,7 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
if (Compat.IsUnix)
{
File.SetUnixFileMode(
UpdateHelper.ExecutablePath, // 0755
UpdateHelper.ExecutablePath.FullPath, // 0755
UnixFileMode.UserRead
| UnixFileMode.UserWrite
| UnixFileMode.UserExecute
@ -170,7 +171,13 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
UpdateText = "Getting a few things ready...";
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...";
@ -259,7 +266,10 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
var currentVersionBlock = results.FindIndex(x => x.Version == currentVersion.WithoutMetadata());
// 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++;
}
@ -267,7 +277,9 @@ public partial class UpdateViewModel : ContentDialogViewModelBase
// Support for previous pre-release without changelogs
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
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.FileInterfaces;
using StabilityMatrix.Core.Models.Settings;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
@ -302,7 +303,10 @@ public partial class MainSettingsViewModel : PageViewModelBase
{
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();
}
});

49
StabilityMatrix.Core/Helper/Compat.cs

@ -4,6 +4,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Semver;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Core.Helper;
@ -62,10 +63,21 @@ public static class Compat
public static DirectoryPath AppCurrentDir { get; }
/// <summary>
/// Current path to the app.
/// Current path to the app binary.
/// </summary>
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
/// <summary>
/// Platform-specific executable extension.
@ -103,7 +115,14 @@ public static class Compat
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
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 = "";
DllExtension = ".dylib";
}
@ -112,11 +131,9 @@ public static class Compat
Platform = PlatformKind.Linux | PlatformKind.Unix;
// For AppImage builds, the path is in `$APPIMAGE`
var appPath =
Environment.GetEnvironmentVariable("APPIMAGE") ?? AppContext.BaseDirectory;
var appPath = Environment.GetEnvironmentVariable("APPIMAGE") ?? AppContext.BaseDirectory;
AppCurrentDir =
Path.GetDirectoryName(appPath)
?? throw new Exception("Could not find application directory");
Path.GetDirectoryName(appPath) ?? throw new Exception("Could not find application directory");
ExeExtension = "";
DllExtension = ".so";
}
@ -186,12 +203,24 @@ public static class Compat
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)
{
var currentPath = Environment.GetEnvironmentVariable(
"PATH",
EnvironmentVariableTarget.Process
);
var currentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process);
var newPath = string.Join(PathDelimiter, paths);
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)
.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;
/// <inheritdoc />

30
StabilityMatrix.Core/Processes/ProcessRunner.cs

@ -29,6 +29,27 @@ public static class ProcessRunner
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>
/// Opens the given folder in the system file explorer.
/// </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
var args = string.Join(" ", commands.Select(Quote));
@ -421,7 +445,9 @@ public static class ProcessRunner
catch (SystemException) { }
throw new ProcessException(
"Process " + (processName == null ? "" : processName + " ") + $"failed with exit-code {process.ExitCode}."
"Process "
+ (processName == null ? "" : processName + " ")
+ $"failed with exit-code {process.ExitCode}."
);
}
}

37
StabilityMatrix.Core/Updater/UpdateHelper.cs

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

Loading…
Cancel
Save