Skip to content

Commit

Permalink
Unpackaged support
Browse files Browse the repository at this point in the history
  • Loading branch information
KimihikoAkayasaki committed Feb 21, 2025
1 parent 741e88e commit c87d114
Show file tree
Hide file tree
Showing 16 changed files with 474 additions and 106 deletions.
112 changes: 73 additions & 39 deletions Amethyst.Plugins.Contract/Classes.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

Expand Down Expand Up @@ -316,48 +317,47 @@ public interface IDependency
public Task<bool> Install(IProgress<InstallationProgress> progress, CancellationToken cancellationToken);
}


// Fix applier worker helper class
public interface IFix
{
/// <summary>
/// [Title]
/// The name of the fix to be applied
/// </summary>
public string Name { get; }

/// <summary>
/// Whether it is a must to have this fix applied
/// (IsNecessary=true) for the plugin to be working properly
/// </summary>
public bool IsMandatory { get; }

/// <summary>
/// Check whether the fix is (already) applied
/// </summary>
public bool IsNecessary { get; }

/// <summary>
/// If there is a need to accept an EULA, pass its contents here
/// </summary>
public string InstallerEula { get; }

/// <summary>
/// Perform the main installation action, reporting the progress
/// </summary>
/// <param name="progress">
/// Report the progress using InstallationProgress
/// </param>
/// <param name="cancellationToken">
/// CancellationToken for task cancellation
/// </param>
/// <param name="arg">
/// Optional argument
/// </param>
/// <returns>
/// Success?
/// </returns>
public Task<bool> Apply(IProgress<InstallationProgress> progress, CancellationToken cancellationToken, object? arg = null);
/// <summary>
/// [Title]
/// The name of the fix to be applied
/// </summary>
public string Name { get; }

/// <summary>
/// Whether it is a must to have this fix applied
/// (IsNecessary=true) for the plugin to be working properly
/// </summary>
public bool IsMandatory { get; }

/// <summary>
/// Check whether the fix is (already) applied
/// </summary>
public bool IsNecessary { get; }

/// <summary>
/// If there is a need to accept an EULA, pass its contents here
/// </summary>
public string InstallerEula { get; }

/// <summary>
/// Perform the main installation action, reporting the progress
/// </summary>
/// <param name="progress">
/// Report the progress using InstallationProgress
/// </param>
/// <param name="cancellationToken">
/// CancellationToken for task cancellation
/// </param>
/// <param name="arg">
/// Optional argument
/// </param>
/// <returns>
/// Success?
/// </returns>
public Task<bool> Apply(IProgress<InstallationProgress> progress, CancellationToken cancellationToken, object? arg = null);
}

// Plugin settings helper class
Expand All @@ -368,4 +368,38 @@ public interface IPluginSettings

// Write a serialized object to the plugin settings
public void SetSetting<T>(object key, T? value);
}

// Path helper class
public interface IPathHelper
{
// Main Amethyst.exe location
public FileInfo ProgramLocation { get; }

// AppData/TempState
public DirectoryInfo TemporaryFolder { get; }

// AppData/LocalState
public DirectoryInfo LocalFolder { get; }

// The location of ALL plugins folder
public Task<DirectoryInfo> GetPluginsFolder();

// The location of plugin working copies
public Task<DirectoryInfo> GetPluginsTempFolder();

// Get file from AppData/LocalState
public Task<FileInfo> GetAppDataFile(string relativeFilePath);

// Get folder from shared (not packed) plugin folder
public Task<DirectoryInfo> GetAppDataPluginFolder(string relativeFilePath);

// Get folder from working copy plugin folder
public Task<DirectoryInfo> GetTempPluginFolder(string relativeFilePath);

// Get file path from AppData/LocalState
public FileInfo GetAppDataFilePath(string relativeFilePath);

// Get log file path from AppData/TempState
public FileInfo GetAppDataLogFilePath(string relativeFilePath);
}
10 changes: 10 additions & 0 deletions Amethyst.Plugins.Contract/Contract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,16 @@ public interface IAmethystHost
/// </summary>
public IPluginSettings PluginSettings { get; }

/// <summary>
/// Helper class that provides path-related methods,
/// as Amethyst may be both packaged and unpackaged,
/// this aims to make path handling easier and safer.
/// In packaged mode, AppData will return shared, packaged AppData
/// and in unpackaged mode, AppData will be created near main exe.
/// This is to ensure portability and prevent the need for cleanup
/// </summary>
public IPathHelper PathHelper { get; }

/// <summary>
/// Get the hook joint pose (typically Head, fallback to .First())
/// of the currently selected base tracking device (no overrides!)
Expand Down
7 changes: 6 additions & 1 deletion Amethyst/Amethyst.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
<AppxBundlePlatforms>x64</AppxBundlePlatforms>
</PropertyGroup>

<PropertyGroup>
<!-- Workaround for MSB3271 error on processor architecture mismatch -->
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CommunityToolkit.WinUI" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
Expand All @@ -62,7 +67,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.10.0" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240627000" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1" />
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.0.8" />
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="RestSharp" Version="108.0.3" />
<PackageReference Include="System.ComponentModel.Composition" Version="8.0.0" />
Expand Down
28 changes: 15 additions & 13 deletions Amethyst/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs;
using System.Text;

// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

namespace Amethyst;

Expand All @@ -61,6 +57,7 @@ public partial class App : Application
public App()
{
InitializeComponent();
AsyncUtils.RunSync(PathsHandler.Setup);

// Listen for and log all uncaught second-chance exceptions : XamlApp
UnhandledException += (_, e) =>
Expand Down Expand Up @@ -296,7 +293,7 @@ protected override async void OnLaunched(LaunchActivatedEventArgs eventArgs)
ElementSoundPlayer.State = ElementSoundPlayerState.Off;

Logger.Info("Creating a new CrashWindow view...");
_crashWindow = new CrashWindow(); // Create a new window
_crashWindow = new CrashWindow(args); // Create a new window

Logger.Info($"Activating {_crashWindow.GetType()}...");
_crashWindow.Activate(); // Activate the main window
Expand Down Expand Up @@ -328,8 +325,13 @@ protected override async void OnLaunched(LaunchActivatedEventArgs eventArgs)
}

// Check if activated via uri
var activationUri = (AppInstance.GetCurrent().GetActivatedEventArgs().Data as
ProtocolActivatedEventArgs)?.Uri;
var activationUri = PathsHandler.IsAmethystPackaged
? (AppInstance.GetCurrent().GetActivatedEventArgs().Data as
ProtocolActivatedEventArgs)?.Uri
: args.Length > 1 && args[1].StartsWith("amethyst-app:") &&
Uri.TryCreate(args[1], UriKind.RelativeOrAbsolute, out var au)
? au
: null;

// Check if there's any launch arguments
if (activationUri is not null && activationUri.Segments.Length > 0)
Expand Down Expand Up @@ -683,7 +685,7 @@ await Interfacing.ExecuteAppRestart(admin: true, filenameOverride: "powershell.e
if (!needToCreateNew)
{
Logger.Fatal(new AbandonedMutexException("Startup failed! The app is already running."));
await "amethyst-app:crash-already-running".ToUri().LaunchAsync();
await "amethyst-app:crash-already-running".Launch();

await Task.Delay(3000);
Environment.Exit(0); // Exit peacefully
Expand All @@ -692,15 +694,15 @@ await Interfacing.ExecuteAppRestart(admin: true, filenameOverride: "powershell.e
catch (Exception e)
{
Logger.Fatal(new AbandonedMutexException($"Startup failed! Mutex creation error: {e.Message}"));
await "amethyst-app:crash-already-running".ToUri().LaunchAsync();
await "amethyst-app:crash-already-running".Launch();

await Task.Delay(3000);
Environment.Exit(0); // Exit peacefully
}

Logger.Info("Starting the crash handler passing the app PID...");
await ($"amethyst-app:crash-watchdog?pid={Environment.ProcessId}&log={Logger.LogFilePath}" +
$"&crash={Interfacing.CrashFile.FullName}").ToUri().LaunchAsync();
$"&crash={Interfacing.CrashFile.FullName}").Launch();

// Disable internal sounds
ElementSoundPlayer.State = ElementSoundPlayerState.Off;
Expand Down Expand Up @@ -884,9 +886,9 @@ await Interfacing.ExecuteAppRestart(admin: true, filenameOverride: "powershell.e
// Update 1.2.13.0: reset configuration
try
{
if (ApplicationData.Current.LocalSettings.Values["SetupFinished"] as bool? ?? false)
if (PathsHandler.LocalSettings["SetupFinished"] as bool? ?? false)
{
ApplicationData.Current.LocalSettings.Values.Remove("SetupFinished");
PathsHandler.LocalSettings.Remove("SetupFinished");
AppData.Settings = new AppSettings(); // Reset application settings
AppData.Settings.SaveSettings(); // Save empty settings to file

Expand All @@ -896,7 +898,7 @@ await Interfacing.ExecuteAppRestart(admin: true, filenameOverride: "powershell.e
"a critical update, so you'll need to reconfigure it.");

// If there's an OVR driver folder, try to delete it if possible
await (await ApplicationData.Current.LocalFolder.GetFolderAsync("Amethyst"))?.DeleteAsync();
await (await PathsHandler.LocalFolder.GetFolderAsync("Amethyst"))?.DeleteAsync();
}
}
catch (Exception ex)
Expand Down
3 changes: 2 additions & 1 deletion Amethyst/Classes/AppData.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Windows.ApplicationModel;
using Amethyst.Utils;
using System.Reflection;

namespace Amethyst.Classes;

Expand All @@ -10,7 +11,7 @@ public static class AppData

// Internal version number
public static (string Display, string Internal)
VersionString => (Package.Current.Id.Version.AsString(), "AZ_BUILD_NUMBER");
VersionString => (Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0.0", "AZ_BUILD_NUMBER");

// Application settings
public static AppSettings Settings { get; set; } = new();
Expand Down
117 changes: 117 additions & 0 deletions Amethyst/Classes/AppDataContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
using Amethyst.Plugins.Contract;
using Amethyst.Utils;
using Newtonsoft.Json;
using Windows.Storage;

namespace Amethyst.Classes;

public class AppDataContainer : INotifyPropertyChanged
{
// ReSharper disable once MemberCanBePrivate.Global
public SortedDictionary<object, object> SettingsDictionary { get; set; } = new();

// MVVM stuff
public event PropertyChangedEventHandler PropertyChanged;

// Save settings
public void SaveSettings()
{
try
{
// Save host settings to $env:AppData/Amethyst/
File.WriteAllText(Interfacing.GetAppDataFilePath("HostSettings.json"),
JsonConvert.SerializeObject(this, Formatting.Indented));
}
catch (Exception e)
{
Logger.Error($"Error saving plugin settings! Message: {e.Message}");
}
}

// Re/Load settings
public void ReadSettings()
{
try
{
// Read host settings from $env:AppData/Amethyst/
SettingsDictionary = (JsonConvert.DeserializeObject<AppDataContainer>(File.ReadAllText(
Interfacing.GetAppDataFilePath("HostSettings.json"))) ?? new AppDataContainer()).SettingsDictionary;
}
catch (Exception e)
{
Logger.Error($"Error reading host settings! Message: {e.Message}");
SettingsDictionary = new SortedDictionary<object, object>(); // Reset if null
}
}

// Save settings
public async Task SaveSettingsAsync(bool silent = false)
{
try
{
// Save host settings to $env:AppData/Amethyst/
await File.WriteAllTextAsync(Interfacing.GetAppDataFilePath("HostSettings.json"),
JsonConvert.SerializeObject(this, Formatting.Indented));
}
catch (Exception e)
{
if (!silent) Logger.Error($"Error saving host settings! Message: {e.Message}");
}
}

// Re/Load settings
public async Task ReadSettingsAsync(bool silent = false)
{
try
{
// Read host settings from $env:AppData/Amethyst/
SettingsDictionary = (JsonConvert.DeserializeObject<AppDataContainer>(await File.ReadAllTextAsync(
Interfacing.GetAppDataFilePath("HostSettings.json"))) ?? new AppDataContainer()).SettingsDictionary;
}
catch (Exception e)
{
if (e is FileNotFoundException) await SaveSettingsAsync(silent);
if (!silent) Logger.Error($"Error reading host settings! Message: {e.Message}");
SettingsDictionary = new SortedDictionary<object, object>(); // Reset if null
}
}

public void OnPropertyChanged(string propName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}

public object this[object key]
{
get => PathsHandler.IsAmethystPackaged
? ApplicationData.Current.LocalSettings.Values[key?.ToString() ?? "INVALID"]
: SettingsDictionary.GetValueOrDefault(key);
set
{
if (PathsHandler.IsAmethystPackaged)
SettingsDictionary[key] = value;
else
ApplicationData.Current.LocalSettings.Values[key?.ToString() ?? "INVALID"] = value;

SaveSettings();
OnPropertyChanged(nameof(SettingsDictionary));
}
}

public void Remove(object key)
{
if (PathsHandler.IsAmethystPackaged)
SettingsDictionary.Remove(key);
else
ApplicationData.Current.LocalSettings.Values.Remove(key?.ToString() ?? "INVALID");

SaveSettings();
OnPropertyChanged(nameof(SettingsDictionary));
}
}
Loading

0 comments on commit c87d114

Please sign in to comment.