mirror of
https://github.com/JosefNemec/Playnite.git
synced 2026-01-09 06:11:22 +08:00
1667 lines
69 KiB
C#
1667 lines
69 KiB
C#
using Playnite.Controllers;
|
|
using Playnite.Input;
|
|
using Playnite.SDK;
|
|
using Playnite.Plugins;
|
|
using Playnite.ViewModels;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Diagnostics;
|
|
using Playnite.Database;
|
|
using Playnite.API;
|
|
using TheArtOfDev.HtmlRenderer;
|
|
using Playnite.Services;
|
|
using System.Windows.Input;
|
|
using System.Windows.Interop;
|
|
using System.Reflection;
|
|
using System.IO;
|
|
using Playnite.Common;
|
|
using System.ComponentModel;
|
|
using Playnite.Windows;
|
|
using Polly;
|
|
using System.Windows.Media;
|
|
using Playnite.SDK.Events;
|
|
using System.Windows.Threading;
|
|
using System.Net;
|
|
using Playnite.Common.Web;
|
|
using System.ServiceProcess;
|
|
using System.Drawing.Imaging;
|
|
|
|
namespace Playnite
|
|
{
|
|
public abstract class PlayniteApplication : ObservableObject, IPlayniteApplication
|
|
{
|
|
private ILogger logger = LogManager.GetLogger();
|
|
private const string instanceMuxet = "PlayniteInstaceMutex";
|
|
private Mutex appMutex;
|
|
public bool ResourcesReleased { get; private set; } = false;
|
|
private PipeService pipeService;
|
|
private PipeServer pipeServer;
|
|
private System.Threading.Timer updateCheckTimer;
|
|
private bool installingAddon = false;
|
|
private AddonLoadError themeLoadError = AddonLoadError.None;
|
|
private ThemeManifest customTheme;
|
|
|
|
private bool isActive;
|
|
public bool IsActive
|
|
{
|
|
get => isActive;
|
|
set
|
|
{
|
|
isActive = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public static Version CurrentVersion => Updater.CurrentVersion;
|
|
public event EventHandler ExtensionsLoaded;
|
|
public ApplicationMode Mode { get; }
|
|
public IDialogsFactory Dialogs { get; set; }
|
|
public PlayniteSettings AppSettings { get; set; }
|
|
public GamesEditor GamesEditor { get; set; }
|
|
public ExtensionFactory Extensions { get; set; }
|
|
public GameDatabase Database { get; set; }
|
|
public GameControllerFactory Controllers { get; set; }
|
|
public CmdLineOptions CmdLine { get; set; }
|
|
public DpiScale DpiScale { get; set; } = new DpiScale(1, 1);
|
|
public ComputerScreen CurrentScreen { get; set; } = Computer.GetPrimaryScreen();
|
|
public DiscordManager Discord { get; set; }
|
|
public SynchronizationContext SyncContext { get; private set; }
|
|
public Action<PlayniteUriEventArgs> AppUriHandler { get; set; }
|
|
public static Application CurrentNative { get; private set; }
|
|
public static PlayniteApplication Current { get; private set; }
|
|
public ServicesClient ServicesClient { get; private set; }
|
|
public MainViewModelBase MainModelBase { get; set; }
|
|
public List<ExtensionInstallResult> ExtensionsInstallResult { get; set; }
|
|
public NotificationsAPI Notifications { get; }
|
|
public PlayniteUriHandler UriHandler { get; }
|
|
public PlayniteAPI PlayniteApiGlobal { get; set; }
|
|
|
|
private ExtensionsStatusBinder extensionsStatusBinder = new ExtensionsStatusBinder();
|
|
public ExtensionsStatusBinder ExtensionsStatusBinder { get => extensionsStatusBinder; set => SetValue(ref extensionsStatusBinder, value); }
|
|
|
|
public PlayniteApplication()
|
|
{
|
|
}
|
|
|
|
public PlayniteApplication(
|
|
Func<Application> appInitializer,
|
|
ApplicationMode mode,
|
|
CmdLineOptions cmdLine)
|
|
{
|
|
if (Current != null)
|
|
{
|
|
throw new Exception("Only one application instance is allowed.");
|
|
}
|
|
|
|
// TODO: remove after switch to .NET 5
|
|
// Fixes various network issues on 2004+ Win10 if TLS 1.3 is forced via registry.
|
|
if (Computer.IsTLS13SystemWideEnabled())
|
|
{
|
|
logger.Warn("System wide TLS 1.3 is enabled, forcing 1.2.");
|
|
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
|
|
}
|
|
|
|
CmdLine = cmdLine;
|
|
Mode = mode;
|
|
Current = this;
|
|
|
|
if (!Debugger.IsAttached)
|
|
{
|
|
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
|
}
|
|
|
|
if (!CmdLine.MasterInstance)
|
|
{
|
|
if (CheckOtherInstances() || CmdLine.Shutdown)
|
|
{
|
|
ResourcesReleased = true;
|
|
Environment.Exit(0);
|
|
return;
|
|
}
|
|
}
|
|
|
|
#if !DEBUG
|
|
if (FileSystem.FileExists(PlaynitePaths.SafeStartupFlagFile))
|
|
{
|
|
if (MessageBox.Show(
|
|
"Playnite closed unexpectedly while starting. This is usually caused by 3rd party theme or extension. Do you want to start in safe mode with all 3rd party add-ons disabled?",
|
|
"Startup Error",
|
|
MessageBoxButton.YesNo,
|
|
MessageBoxImage.Warning) == MessageBoxResult.Yes)
|
|
{
|
|
cmdLine.SafeStartup = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FileSystem.CreateFile(PlaynitePaths.SafeStartupFlagFile);
|
|
}
|
|
#endif
|
|
|
|
// All code above has to be called before we create instance of WPF app,
|
|
// because MessageBox forces WPF to initialize and fire startup app events.
|
|
CurrentNative = appInitializer();
|
|
CurrentNative.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
|
SyncContext = new DispatcherSynchronizationContext(CurrentNative.Dispatcher);
|
|
SynchronizationContext.SetSynchronizationContext(SyncContext);
|
|
appMutex = new Mutex(true, instanceMuxet);
|
|
|
|
try
|
|
{
|
|
// This can fail in rare cases when switching application modes
|
|
// if an old instance fails to clean after itself or if it gets stuck on exit.
|
|
Policy.Handle<Exception>()
|
|
.WaitAndRetry(3, a => TimeSpan.FromSeconds(3))
|
|
.Execute(() => pipeService = new PipeService());
|
|
pipeService.CommandExecuted += PipeService_CommandExecuted;
|
|
pipeServer = new PipeServer(PlayniteSettings.GetAppConfigValue("PipeEndpoint"));
|
|
pipeServer.StartServer(pipeService);
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(exc, "Failed to start pipe service.");
|
|
}
|
|
|
|
PlayniteSettings.MigrateSettingsConfig();
|
|
AppSettings = PlayniteSettings.LoadSettings();
|
|
Commands.GlobalCommands.AppSettings = AppSettings;
|
|
NLogLogger.IsTraceEnabled = AppSettings.TraceLogEnabled;
|
|
|
|
if (AppSettings.ShouldDataBackupOnStartup())
|
|
{
|
|
var backOptions = Backup.GetAutoBackupOptions(AppSettings, PlaynitePaths.ConfigRootPath, GameDatabase.GetFullDbPath(AppSettings.DatabasePath));
|
|
FileSystem.WriteStringToFile(PlaynitePaths.BackupActionFile, Serialization.ToJson(backOptions));
|
|
CmdLine.Backup = PlaynitePaths.BackupActionFile;
|
|
AppSettings.LastAutoBackup = DateTime.Now;
|
|
AppSettings.SaveSettings();
|
|
}
|
|
|
|
if (!CmdLine.Backup.IsNullOrEmpty() || !CmdLine.RestoreBackup.IsNullOrEmpty())
|
|
{
|
|
ServicesClient = new ServicesClient();
|
|
CurrentNative.SessionEnding += Application_SessionEnding;
|
|
CurrentNative.Exit += Application_Exit;
|
|
CurrentNative.Startup += Application_Startup;
|
|
CurrentNative.Activated += Application_Activated;
|
|
CurrentNative.Deactivated += Application_Deactivated;
|
|
|
|
var defaultTheme = new ThemeManifest()
|
|
{
|
|
DirectoryName = ThemeManager.DefaultThemeDirName,
|
|
DirectoryPath = Path.Combine(PlaynitePaths.ThemesProgramPath, ThemeManager.GetThemeRootDir(Mode), ThemeManager.DefaultThemeDirName),
|
|
Name = ThemeManager.DefaultThemeDirName,
|
|
Id = mode == ApplicationMode.Desktop ? ThemeManager.DefaultDesktopThemeId : ThemeManager.DefaultFullscreenThemeId
|
|
};
|
|
|
|
ThemeManager.SetDefaultTheme(defaultTheme);
|
|
InitializeNative();
|
|
|
|
try
|
|
{
|
|
Localization.SetLanguage(AppSettings.Language);
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(exc, $"Failed to set {AppSettings.Language} langauge.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (CmdLine.ResetSettings)
|
|
{
|
|
var settings = PlayniteSettings.GetDefaultSettings();
|
|
settings.FirstTimeWizardComplete = true;
|
|
settings.DatabasePath = AppSettings.DatabasePath;
|
|
settings.SaveSettings();
|
|
AppSettings = settings;
|
|
}
|
|
|
|
var relaunchPath = string.Empty;
|
|
if (AppSettings.StartInFullscreen && mode == ApplicationMode.Desktop && !CmdLine.StartInDesktop)
|
|
{
|
|
relaunchPath = PlaynitePaths.FullscreenExecutablePath;
|
|
}
|
|
|
|
if (CmdLine.StartInDesktop && mode != ApplicationMode.Desktop)
|
|
{
|
|
relaunchPath = PlaynitePaths.DesktopExecutablePath;
|
|
}
|
|
else if (CmdLine.StartInFullscreen && mode != ApplicationMode.Fullscreen)
|
|
{
|
|
relaunchPath = PlaynitePaths.FullscreenExecutablePath;
|
|
}
|
|
|
|
if (!relaunchPath.IsNullOrEmpty())
|
|
{
|
|
FileSystem.DeleteFile(PlaynitePaths.SafeStartupFlagFile);
|
|
ProcessStarter.StartProcess(relaunchPath, CmdLine.ToString());
|
|
CurrentNative.Shutdown(0);
|
|
return;
|
|
}
|
|
|
|
ServicesClient = new ServicesClient();
|
|
CurrentNative.SessionEnding += Application_SessionEnding;
|
|
CurrentNative.Exit += Application_Exit;
|
|
CurrentNative.Startup += Application_Startup;
|
|
CurrentNative.Activated += Application_Activated;
|
|
CurrentNative.Deactivated += Application_Deactivated;
|
|
|
|
OnPropertyChanged(nameof(AppSettings));
|
|
var defaultTheme = new ThemeManifest()
|
|
{
|
|
DirectoryName = ThemeManager.DefaultThemeDirName,
|
|
DirectoryPath = Path.Combine(PlaynitePaths.ThemesProgramPath, ThemeManager.GetThemeRootDir(Mode), ThemeManager.DefaultThemeDirName),
|
|
Name = ThemeManager.DefaultThemeDirName,
|
|
Id = mode == ApplicationMode.Desktop ? ThemeManager.DefaultDesktopThemeId : ThemeManager.DefaultFullscreenThemeId
|
|
};
|
|
|
|
try
|
|
{
|
|
WaitForOtherInstacesToExit(false);
|
|
ExtensionsInstallResult = ExtensionInstaller.InstallExtensionQueue();
|
|
var installedTheme = ExtensionsInstallResult.FirstOrDefault(a => a.InstalledManifest is ThemeManifest && !a.Updated);
|
|
if (installedTheme?.InstalledManifest != null)
|
|
{
|
|
var theme = installedTheme.InstalledManifest as ThemeManifest;
|
|
if (theme.Mode == Mode)
|
|
{
|
|
if (theme.Mode == ApplicationMode.Desktop)
|
|
{
|
|
AppSettings.Theme = theme.Id;
|
|
}
|
|
else
|
|
{
|
|
AppSettings.Fullscreen.Theme = theme.Id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(e, "Failed to finish installing extenions.");
|
|
}
|
|
|
|
ThemeManager.SetDefaultTheme(defaultTheme);
|
|
|
|
// Theme must be set BEFORE default app resources are initialized for ThemeFile markup to apply custom theme's paths.
|
|
customTheme = null;
|
|
if (CmdLine.ForceDefaultTheme || CmdLine.SafeStartup)
|
|
{
|
|
logger.Warn("Default theme forced by cmdline.");
|
|
}
|
|
else
|
|
{
|
|
var theme = mode == ApplicationMode.Desktop ? AppSettings.Theme : AppSettings.Fullscreen.Theme;
|
|
if (theme != ThemeManager.DefaultTheme.Id)
|
|
{
|
|
customTheme = ThemeManager.GetAvailableThemes(mode).Where(a => a.Id == theme).OrderByDescending(a => a.Version).FirstOrDefault();
|
|
if (customTheme == null)
|
|
{
|
|
logger.Error($"Failed to apply theme {theme}, theme not found.");
|
|
if (mode == ApplicationMode.Desktop)
|
|
{
|
|
AppSettings.Theme = ThemeManager.DefaultDesktopThemeId;
|
|
}
|
|
else
|
|
{
|
|
AppSettings.Fullscreen.Theme = ThemeManager.DefaultFullscreenThemeId;
|
|
}
|
|
|
|
ThemeManager.SetCurrentTheme(defaultTheme);
|
|
}
|
|
else
|
|
{
|
|
ThemeManager.SetCurrentTheme(customTheme);
|
|
}
|
|
}
|
|
}
|
|
|
|
InitializeNative();
|
|
|
|
try
|
|
{
|
|
if (!AppSettings.FirstTimeWizardComplete)
|
|
{
|
|
var cultName = System.Globalization.CultureInfo.CurrentUICulture.Name.Replace('-', '_');
|
|
var validLang = Localization.AvailableLanguages.FirstOrDefault(a => a.Id == cultName && a.TranslatedPercentage > 75);
|
|
if (validLang != null)
|
|
{
|
|
AppSettings.Language = validLang.Id;
|
|
}
|
|
}
|
|
|
|
Localization.SetLanguage(AppSettings.Language);
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(exc, $"Failed to set {AppSettings.Language} langauge.");
|
|
}
|
|
|
|
// Must be applied AFTER default app resources are initialized, otherwise custom resource dictionaries won't be properly added to application scope.
|
|
if (customTheme != null)
|
|
{
|
|
themeLoadError = ThemeManager.ApplyTheme(CurrentNative, customTheme, Mode);
|
|
if (themeLoadError != AddonLoadError.None)
|
|
{
|
|
ThemeManager.SetCurrentTheme(null);
|
|
logger.Error($"Failed to load theme {customTheme.Name}, {themeLoadError}.");
|
|
}
|
|
}
|
|
|
|
if (mode == ApplicationMode.Desktop)
|
|
{
|
|
try
|
|
{
|
|
if (System.Drawing.FontFamily.Families.Any(a => a.Name == AppSettings.FontFamilyName))
|
|
{
|
|
CurrentNative.Resources.Add(
|
|
"FontFamily", new FontFamily(AppSettings.FontFamilyName));
|
|
}
|
|
else
|
|
{
|
|
logger.Error($"Cannot set font {AppSettings.FontFamilyName}, font not found.");
|
|
}
|
|
|
|
if (System.Drawing.FontFamily.Families.Any(a => a.Name == AppSettings.MonospaceFontFamilyName))
|
|
{
|
|
CurrentNative.Resources.Add(
|
|
"MonospaceFontFamily", new FontFamily(AppSettings.MonospaceFontFamilyName));
|
|
}
|
|
else
|
|
{
|
|
logger.Error($"Cannot set monospace font {AppSettings.MonospaceFontFamilyName}, font not found.");
|
|
}
|
|
|
|
if (AppSettings.FontSize > 0)
|
|
{
|
|
CurrentNative.Resources.Add(
|
|
"FontSize", AppSettings.FontSize);
|
|
}
|
|
|
|
if (AppSettings.FontSizeSmall > 0)
|
|
{
|
|
CurrentNative.Resources.Add(
|
|
"FontSizeSmall", AppSettings.FontSizeSmall);
|
|
}
|
|
|
|
if (AppSettings.FontSizeLarge > 0)
|
|
{
|
|
CurrentNative.Resources.Add(
|
|
"FontSizeLarge", AppSettings.FontSizeLarge);
|
|
}
|
|
|
|
if (AppSettings.FontSizeLarger > 0)
|
|
{
|
|
CurrentNative.Resources.Add(
|
|
"FontSizeLarger", AppSettings.FontSizeLarger);
|
|
}
|
|
|
|
if (AppSettings.FontSizeLargest > 0)
|
|
{
|
|
CurrentNative.Resources.Add(
|
|
"FontSizeLargest", AppSettings.FontSizeLargest);
|
|
}
|
|
}
|
|
catch (Exception e) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(e, $"Failed to set font {AppSettings.FontFamilyName}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (AppSettings.Fullscreen.FontSize > 0)
|
|
{
|
|
CurrentNative.Resources.Add(
|
|
"FontSize", AppSettings.Fullscreen.FontSize);
|
|
}
|
|
|
|
if (AppSettings.Fullscreen.FontSizeSmall > 0)
|
|
{
|
|
CurrentNative.Resources.Add(
|
|
"FontSizeSmall", AppSettings.Fullscreen.FontSizeSmall);
|
|
}
|
|
}
|
|
|
|
// Only use this for Desktop mode. Non-default options look terrible in Fullscreen because of viewport scaling.
|
|
if (mode == ApplicationMode.Desktop)
|
|
{
|
|
Controls.WindowBase.SetTextRenderingOptions(AppSettings.TextFormattingMode, AppSettings.TextRenderingMode);
|
|
}
|
|
|
|
Notifications = new NotificationsAPI();
|
|
UriHandler = new PlayniteUriHandler();
|
|
}
|
|
}
|
|
|
|
public abstract void InstantiateApp();
|
|
|
|
public abstract void InitializeNative();
|
|
|
|
public abstract void Restore();
|
|
|
|
public abstract void Minimize();
|
|
|
|
public abstract void ShowWindowsNotification(string title, string body, Action action);
|
|
|
|
public abstract void SwitchAppMode(ApplicationMode mode);
|
|
|
|
public abstract void ConfigureViews();
|
|
|
|
private void Application_SessionEnding(object sender, SessionEndingCancelEventArgs e)
|
|
{
|
|
logger.Info("Shutting down application because of session ending.");
|
|
// Don't dispose CefSharp here because of bug in CefSharp during system shutdown
|
|
// https://github.com/JosefNemec/Playnite/issues/866
|
|
AppSettings?.SaveSettings();
|
|
ReleaseResources(false);
|
|
CurrentNative.Shutdown(0);
|
|
}
|
|
|
|
private void Application_Exit(object sender, ExitEventArgs e)
|
|
{
|
|
ReleaseResources();
|
|
}
|
|
|
|
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
|
{
|
|
// Running under Wine is not supported
|
|
if (PlayniteProcess.WorkingSetMemory == 0 &&
|
|
Programs.GetUnistallProgramsList().Any(a => a.DisplayName.StartsWith("Wine Mono", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
Process.GetCurrentProcess().Kill();
|
|
return;
|
|
}
|
|
|
|
var exception = (Exception)e.ExceptionObject;
|
|
var crashInfo = Exceptions.GetExceptionInfo(exception, Extensions);
|
|
logger.Error(exception, $"Unhandled exception occured.");
|
|
logger.Error($"HResult: 0x{exception.HResult:X8}");
|
|
if (exception is Win32Exception win32exc)
|
|
logger.Error($"Win32 NativeErrorCode: 0x{win32exc.NativeErrorCode:X8}");
|
|
CrashHandlerViewModel crashModel = null;
|
|
|
|
// Delete safe startup flag if we are able to handle the crash,
|
|
// safe startup option should show for crashes we are not handling.
|
|
FileSystem.DeleteFile(PlaynitePaths.SafeStartupFlagFile);
|
|
if (crashInfo.IsLiteDbCorruptionCrash)
|
|
{
|
|
Dialogs.ShowErrorMessage(LOC.DBCorruptionCrashMessage.GetLocalized());
|
|
Process.GetCurrentProcess().Kill();
|
|
return;
|
|
}
|
|
|
|
if (crashInfo.IsExtensionCrash)
|
|
{
|
|
crashModel = new CrashHandlerViewModel(
|
|
new ExtensionCrashHandlerWindowFactory(),
|
|
Dialogs,
|
|
new ResourceProvider(),
|
|
Mode,
|
|
crashInfo,
|
|
AppSettings);
|
|
}
|
|
else
|
|
{
|
|
// unchecked use reason: https://stackoverflow.com/a/10043486/1107424
|
|
|
|
// Have nonsense crashes with this about normal .NET runtime methods and Playnite class methods missing.
|
|
if (exception is MissingMethodException ||
|
|
exception is BadImageFormatException ||
|
|
exception is InvalidProgramException ||
|
|
// Looks like there are some nested TargetInvocationException with MissingMethodException actual extension,
|
|
// which seems to look like corrupted installed where binaries from different version got mixed up.
|
|
exception.StackTrace?.Contains("System.MissingMethodException") == true ||
|
|
// Usually COM execution error from WindowsAPICodePack when opening folder selection dialog. As far as I can tell, this happens on "debloated" Windows edition only.
|
|
(exception is System.Runtime.InteropServices.COMException &&
|
|
(exception.HResult == unchecked((int)0x80004005) || exception.HResult == unchecked((int)0x80040111))) ||
|
|
// DWM_E_COMPOSITIONDISABLED, looks like this can happen when GPU driver crashes and doesn't reboot properly
|
|
exception.HResult == unchecked((int)0x80263001))
|
|
{
|
|
Dialogs.ShowErrorMessage("System issue or corrupted Playnite install detected.");
|
|
Process.GetCurrentProcess().Kill();
|
|
return;
|
|
}
|
|
|
|
// ERROR_DISK_FULL
|
|
if (exception.HResult == unchecked((int)0x80070070) ||
|
|
// "device not ready" error. Happens when people run Playnite from attached storage as far as I can tell.
|
|
exception.HResult == unchecked((int)0x80070015) ||
|
|
// self-explanatory
|
|
exception is OutOfMemoryException)
|
|
{
|
|
Dialogs.ShowErrorMessage(exception.Message, LOC.CrashWindowTitle.GetLocalized());
|
|
Process.GetCurrentProcess().Kill();
|
|
return;
|
|
}
|
|
|
|
crashModel = new CrashHandlerViewModel(
|
|
new CrashHandlerWindowFactory(),
|
|
Dialogs,
|
|
new ResourceProvider(),
|
|
Mode);
|
|
}
|
|
|
|
crashModel.OpenView();
|
|
Process.GetCurrentProcess().Kill();
|
|
}
|
|
|
|
private void Application_Startup(object sender, StartupEventArgs e)
|
|
{
|
|
logger.Info($"Application started from '{PlaynitePaths.ProgramPath}'");
|
|
SDK.Data.Markup.Init(new MarkupConverter());
|
|
SDK.Data.Serialization.Init(new DataSerializer());
|
|
SDK.Data.SQLite.Init((a, b) => new Sqlite(a, b));
|
|
EventManager.RegisterClassHandler(typeof(Controls.WindowBase), Controls.WindowBase.ClosedRoutedEvent, new RoutedEventHandler(WindowBaseCloseHandler));
|
|
EventManager.RegisterClassHandler(typeof(Controls.WindowBase), Controls.WindowBase.LoadedRoutedEvent, new RoutedEventHandler(WindowBaseLoadedHandler));
|
|
ConfigureViews();
|
|
|
|
if (!CmdLine.Backup.IsNullOrEmpty())
|
|
{
|
|
BackupOptions backupOptions = null;
|
|
try
|
|
{
|
|
backupOptions = Serialization.FromJsonFile<BackupOptions>(CmdLine.Backup);
|
|
var progRes = Dialogs.ActivateGlobalProgress(
|
|
(progArgs) =>
|
|
{
|
|
WaitForOtherInstacesToExit(true);
|
|
Backup.BackupData(backupOptions, progArgs.CancelToken);
|
|
},
|
|
new GlobalProgressOptions(LOC.BackupProgress, true) { IsIndeterminate = true });
|
|
if (progRes.Error != null)
|
|
{
|
|
logger.Error(progRes.Error, "Failed to backup data.");
|
|
throw progRes.Error;
|
|
}
|
|
|
|
if (progRes.Canceled)
|
|
{
|
|
Dialogs.ShowErrorMessage(LOC.BackupCancelled, LOC.BackupErrorTitle);
|
|
}
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
Dialogs.ShowErrorMessage(LOC.BackupFailed.GetLocalized() + Environment.NewLine + Environment.NewLine + exc.Message, LOC.BackupErrorTitle);
|
|
logger.Error(exc, "Failed to backup data.");
|
|
}
|
|
finally
|
|
{
|
|
if (backupOptions == null || !backupOptions.ClosedWhenDone)
|
|
{
|
|
Restart(new CmdLineOptions
|
|
{
|
|
StartClosedToTray = CmdLine.StartClosedToTray,
|
|
HideSplashScreen = CmdLine.HideSplashScreen,
|
|
StartInFullscreen = CmdLine.StartInFullscreen
|
|
}, false);
|
|
}
|
|
}
|
|
|
|
FileSystem.DeleteFile(PlaynitePaths.SafeStartupFlagFile);
|
|
Quit(false);
|
|
return;
|
|
}
|
|
else if (!CmdLine.RestoreBackup.IsNullOrEmpty())
|
|
{
|
|
BackupRestoreOptions restoreOptions = null;
|
|
try
|
|
{
|
|
restoreOptions = Serialization.FromJsonFile<BackupRestoreOptions>(CmdLine.RestoreBackup);
|
|
var progRes = Dialogs.ActivateGlobalProgress(
|
|
(progArgs) =>
|
|
{
|
|
WaitForOtherInstacesToExit(true);
|
|
Backup.RestoreBackup(restoreOptions);
|
|
},
|
|
new GlobalProgressOptions(LOC.BackupRestoreProgress, false) { IsIndeterminate = true });
|
|
if (progRes.Error != null)
|
|
{
|
|
logger.Error(progRes.Error, "Failed to restore data from backup.");
|
|
throw progRes.Error;
|
|
}
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
Dialogs.ShowErrorMessage(LOC.BackupRestoreFailed.GetLocalized() + Environment.NewLine + Environment.NewLine + exc.Message, LOC.BackupErrorTitle);
|
|
logger.Error(exc, "Failed to restore data from backup.");
|
|
}
|
|
finally
|
|
{
|
|
if (restoreOptions == null || !restoreOptions.ClosedWhenDone)
|
|
{
|
|
Restart(new CmdLineOptions(), false);
|
|
}
|
|
}
|
|
|
|
FileSystem.DeleteFile(PlaynitePaths.SafeStartupFlagFile);
|
|
Quit(false);
|
|
return;
|
|
}
|
|
|
|
if (!Startup())
|
|
{
|
|
Quit();
|
|
return;
|
|
}
|
|
|
|
logger.Info($"Application {CurrentVersion} started");
|
|
|
|
ExtensionsInstallResult?.Where(a => a.InstallError != null).ForEach(ext =>
|
|
Notifications.Add(new NotificationMessage(
|
|
"inst_err" + ext.PackagePath,
|
|
ResourceProvider.GetString(LOC.AddonInstallFaild).Format(Path.GetFileNameWithoutExtension(ext.PackagePath)) +
|
|
"\n" + ext.InstallError.Message,
|
|
NotificationType.Error)));
|
|
|
|
foreach (var fail in Extensions.FailedExtensions)
|
|
{
|
|
Notifications.Add(new NotificationMessage(
|
|
fail.manifest.DirectoryPath,
|
|
fail.error == AddonLoadError.SDKVersion ?
|
|
ResourceProvider.GetString(LOC.SpecificExtensionLoadSDKError).Format(fail.manifest.Name) :
|
|
ResourceProvider.GetString(LOC.SpecificExtensionLoadError).Format(fail.manifest.Name),
|
|
NotificationType.Error));
|
|
}
|
|
|
|
if (themeLoadError != AddonLoadError.None && customTheme != null)
|
|
{
|
|
Notifications.Add(new NotificationMessage(
|
|
customTheme.DirectoryPath,
|
|
themeLoadError == AddonLoadError.SDKVersion ?
|
|
ResourceProvider.GetString(LOC.SpecificThemeLoadSDKError).Format(customTheme.Name) :
|
|
ResourceProvider.GetString(LOC.SpecificThemeLoadError).Format(customTheme.Name),
|
|
NotificationType.Error));
|
|
}
|
|
|
|
try
|
|
{
|
|
if (AppSettings.ShowNahimicServiceWarning)
|
|
{
|
|
if (ServiceController.GetServices().FirstOrDefault(a =>
|
|
(a.ServiceName?.Contains("nahimic", StringComparison.OrdinalIgnoreCase) == true ||
|
|
a.DisplayName?.Contains("nahimic", StringComparison.OrdinalIgnoreCase) == true) &&
|
|
a.Status != ServiceControllerStatus.Stopped) != null)
|
|
{
|
|
var okResponse = new MessageBoxOption(LOC.OKLabel, true, true);
|
|
var dontShowResponse = new MessageBoxOption(LOC.DontShowAgainTitle);
|
|
var res = Dialogs.ShowMessage(
|
|
LOC.NahimicServiceWarning, "",
|
|
MessageBoxImage.Warning,
|
|
new List<MessageBoxOption> { okResponse, dontShowResponse });
|
|
if (res == dontShowResponse)
|
|
{
|
|
AppSettings.ShowNahimicServiceWarning = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception nahExc)
|
|
{
|
|
// ServiceController.GetServices() can apparently blow up on Win32Exception sometimes
|
|
logger.Error(nahExc, "Failed to check for Nahimic service.");
|
|
}
|
|
|
|
if (PlayniteEnvironment.IsElevated && AppSettings.ShowElevatedRightsWarning)
|
|
{
|
|
var okResponse = new MessageBoxOption(LOC.OKLabel, true, true);
|
|
var dontShowResponse = new MessageBoxOption(LOC.DontShowAgainTitle);
|
|
var res = Dialogs.ShowMessage(
|
|
LOC.ElevatedProcessWarning, "",
|
|
MessageBoxImage.Warning,
|
|
new List<MessageBoxOption> { okResponse, dontShowResponse });
|
|
if (res == dontShowResponse)
|
|
{
|
|
AppSettings.ShowElevatedRightsWarning = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void WindowBaseCloseHandler(object sender, RoutedEventArgs e)
|
|
{
|
|
WindowManager.NotifyChildOwnershipChanges();
|
|
}
|
|
|
|
private void WindowBaseLoadedHandler(object sender, RoutedEventArgs e)
|
|
{
|
|
WindowManager.NotifyChildOwnershipChanges();
|
|
}
|
|
|
|
private void PipeService_CommandExecuted(object sender, CommandExecutedEventArgs args)
|
|
{
|
|
logger.Info($"Executing command \"{args.Command}\" from pipe with arguments \"{args.Args}\"");
|
|
|
|
switch (args.Command)
|
|
{
|
|
case CmdlineCommand.Focus:
|
|
Restore();
|
|
break;
|
|
|
|
case CmdlineCommand.Start:
|
|
if (Guid.TryParse(args.Args, out var gameId))
|
|
{
|
|
var game = Database.Games[gameId];
|
|
if (game == null)
|
|
{
|
|
logger.Error($"Cannot start game, game {args.Args} not found.");
|
|
}
|
|
else
|
|
{
|
|
GamesEditor.PlayGame(game, false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logger.Error($"Can't start game, failed to parse game id: {args.Args}");
|
|
}
|
|
|
|
break;
|
|
|
|
case CmdlineCommand.UriRequest:
|
|
UriHandler.ProcessUri(args.Args);
|
|
break;
|
|
|
|
case CmdlineCommand.ExtensionInstall:
|
|
if (installingAddon)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var extPath = args.Args;
|
|
if (!File.Exists(extPath))
|
|
{
|
|
logger.Error($"Cannot install extension, file doesn't exists: {extPath}");
|
|
return;
|
|
}
|
|
|
|
installingAddon = true;
|
|
var ext = Path.GetExtension(extPath).ToLower();
|
|
if (ext.Equals(PlaynitePaths.PackedThemeFileExtention, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
MainModelBase.Window.RestoreWindow();
|
|
InstallThemeFile(extPath);
|
|
}
|
|
else if (ext.Equals(PlaynitePaths.PackedExtensionFileExtention, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
MainModelBase.Window.RestoreWindow();
|
|
InstallExtensionFile(extPath);
|
|
}
|
|
|
|
installingAddon = false;
|
|
break;
|
|
|
|
case CmdlineCommand.SwitchMode:
|
|
if (args.Args == "desktop")
|
|
{
|
|
SyncContext.Post(_ => SwitchAppMode(ApplicationMode.Desktop), null);
|
|
}
|
|
else if (args.Args == "fullscreen")
|
|
{
|
|
SyncContext.Post(_ => SwitchAppMode(ApplicationMode.Fullscreen), null);
|
|
}
|
|
else
|
|
{
|
|
logger.Error($"Can't switch to uknwon application mode: {args.Args}");
|
|
}
|
|
break;
|
|
|
|
case CmdlineCommand.Shutdown:
|
|
Quit();
|
|
break;
|
|
|
|
case CmdlineCommand.BackupData:
|
|
if (!File.Exists(args.Args))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var backupOptions = Serialization.FromJsonFile<BackupOptions>(args.Args);
|
|
if (backupOptions.CancelIfGameRunning && GamesEditor.RunningGames.HasItems())
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
Restart(new CmdLineOptions
|
|
{
|
|
Backup = args.Args
|
|
});
|
|
}
|
|
break;
|
|
|
|
case CmdlineCommand.RestoreBackup:
|
|
if (!File.Exists(args.Args))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var restoreOptions = Serialization.FromJsonFile<BackupRestoreOptions>(args.Args);
|
|
if (restoreOptions.CancelIfGameRunning && GamesEditor.RunningGames.HasItems())
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
Restart(new CmdLineOptions
|
|
{
|
|
RestoreBackup = args.Args
|
|
});
|
|
}
|
|
break;
|
|
|
|
default:
|
|
logger.Warn("Unknown command received");
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void Application_Activated(object sender, EventArgs e)
|
|
{
|
|
IsActive = true;
|
|
}
|
|
|
|
private void Application_Deactivated(object sender, EventArgs e)
|
|
{
|
|
IsActive = false;
|
|
}
|
|
|
|
public void Run()
|
|
{
|
|
CurrentNative.Run();
|
|
}
|
|
|
|
public abstract bool Startup();
|
|
|
|
public bool CheckOtherInstances()
|
|
{
|
|
var curProcess = Process.GetCurrentProcess();
|
|
if (Mutex.TryOpenExisting(instanceMuxet, out var mutex))
|
|
{
|
|
try
|
|
{
|
|
Policy.Handle<Exception>()
|
|
.WaitAndRetry(3, a => TimeSpan.FromSeconds(3))
|
|
.Execute(() =>
|
|
{
|
|
var client = new PipeClient(PlayniteSettings.GetAppConfigValue("PipeEndpoint"));
|
|
if (!CmdLine.Start.IsNullOrEmpty())
|
|
{
|
|
client.InvokeCommand(CmdlineCommand.Start, CmdLine.Start);
|
|
}
|
|
else if (!CmdLine.UriData.IsNullOrEmpty())
|
|
{
|
|
client.InvokeCommand(CmdlineCommand.UriRequest, CmdLine.UriData);
|
|
}
|
|
else if (!CmdLine.InstallExtension.IsNullOrEmpty())
|
|
{
|
|
client.InvokeCommand(CmdlineCommand.ExtensionInstall, CmdLine.InstallExtension);
|
|
}
|
|
else if (CmdLine.StartInDesktop)
|
|
{
|
|
client.InvokeCommand(CmdlineCommand.SwitchMode, "desktop");
|
|
}
|
|
else if (CmdLine.StartInFullscreen)
|
|
{
|
|
client.InvokeCommand(CmdlineCommand.SwitchMode, "fullscreen");
|
|
}
|
|
else if (CmdLine.Shutdown)
|
|
{
|
|
client.InvokeCommand(CmdlineCommand.Shutdown, null);
|
|
}
|
|
else if (!CmdLine.Backup.IsNullOrEmpty())
|
|
{
|
|
client.InvokeCommand(CmdlineCommand.BackupData, CmdLine.Backup);
|
|
}
|
|
else if (!CmdLine.RestoreBackup.IsNullOrEmpty())
|
|
{
|
|
client.InvokeCommand(CmdlineCommand.RestoreBackup, CmdLine.RestoreBackup);
|
|
}
|
|
else
|
|
{
|
|
var existingProcess = Process.GetProcesses().
|
|
First(a => IsProcessPlayniteProcess(a) && a.Id != curProcess.Id);
|
|
if (existingProcess.ProcessName == curProcess.ProcessName)
|
|
{
|
|
client.InvokeCommand(CmdlineCommand.Focus, string.Empty);
|
|
}
|
|
else
|
|
{
|
|
client.InvokeCommand(CmdlineCommand.SwitchMode, Mode == ApplicationMode.Desktop ? "desktop" : "fullscreen");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
MessageBox.Show(
|
|
"Playnite failed to start. Please close all other instances and try again.",
|
|
"Startup Error");
|
|
logger.Error(exc, "Can't process communication with other instances.");
|
|
}
|
|
|
|
logger.Info("Application already running, shutting down.");
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
var processes = Process.GetProcesses().Where(a => IsProcessPlayniteProcess(a)).ToList();
|
|
// In case multiple processes end up in this branch,
|
|
// the process with highest process id gets to live.
|
|
if (processes.Count > 1 && processes.Max(a => a.Id) != curProcess.Id)
|
|
{
|
|
logger.Info("Another process instance(s) is already running, shutting down.");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool ConfigureApplication()
|
|
{
|
|
HtmlRendererSettings.ImageCachePath = PlaynitePaths.ImagesCachePath;
|
|
HtmlRendererSettings.ImageLoader = BitmapExtensions.HtmlComponentImageLoader;
|
|
if (AppSettings.DisableHwAcceleration || CmdLine.ForceSoftwareRender)
|
|
{
|
|
logger.Info("Enabling software rendering.");
|
|
System.Windows.Media.RenderOptions.ProcessRenderMode = System.Windows.Interop.RenderMode.SoftwareOnly;
|
|
}
|
|
|
|
if (CmdLine.ClearWebCache)
|
|
{
|
|
try
|
|
{
|
|
FileSystem.DeleteDirectory(PlaynitePaths.BrowserCachePath);
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(exc, "Failed to clear CEF cache.");
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
CefTools.ConfigureCef(AppSettings.TraceLogEnabled);
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(exc, "Failed to initialize CefSharp.");
|
|
}
|
|
|
|
if (!CefTools.IsInitialized)
|
|
{
|
|
Dialogs.ShowErrorMessage(
|
|
ResourceProvider.GetString("LOCCefSharpInitError"),
|
|
ResourceProvider.GetString("LOCStartupError"));
|
|
Quit();
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
ExtensionFactory.CreatePluginFolders();
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(exc, "Failed to script and plugin directories.");
|
|
}
|
|
|
|
try
|
|
{
|
|
SystemIntegration.SetBootupStateRegistration(AppSettings.StartOnBoot, AppSettings.StartOnBootClosedToTray);
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(exc, "Failed to register Playnite to start on boot.");
|
|
}
|
|
|
|
try
|
|
{
|
|
SystemIntegration.RegisterPlayniteUriProtocol();
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(exc, "Failed to register playnite URI scheme.");
|
|
}
|
|
|
|
try
|
|
{
|
|
SystemIntegration.RegisterFileExtensions();
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(exc, "Failed to register playnite extensions.");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void ProcessArguments()
|
|
{
|
|
UriHandler.Handlers.Add("playnite", ProcessUriRequest);
|
|
if (!CmdLine.Start.IsNullOrEmpty())
|
|
{
|
|
PipeService_CommandExecuted(this, new CommandExecutedEventArgs(CmdlineCommand.Start, CmdLine.Start));
|
|
}
|
|
else if (!CmdLine.UriData.IsNullOrEmpty())
|
|
{
|
|
PipeService_CommandExecuted(this, new CommandExecutedEventArgs(CmdlineCommand.UriRequest, CmdLine.UriData));
|
|
}
|
|
else if (!CmdLine.InstallExtension.IsNullOrEmpty())
|
|
{
|
|
PipeService_CommandExecuted(this, new CommandExecutedEventArgs(CmdlineCommand.ExtensionInstall, CmdLine.InstallExtension));
|
|
}
|
|
else if (CmdLine.StartInDesktop)
|
|
{
|
|
PipeService_CommandExecuted(this, new CommandExecutedEventArgs(CmdlineCommand.SwitchMode, "desktop"));
|
|
}
|
|
else if (CmdLine.StartInFullscreen)
|
|
{
|
|
PipeService_CommandExecuted(this, new CommandExecutedEventArgs(CmdlineCommand.SwitchMode, "fullscreen"));
|
|
}
|
|
else if (CmdLine.Shutdown)
|
|
{
|
|
PipeService_CommandExecuted(this, new CommandExecutedEventArgs(CmdlineCommand.Shutdown, null));
|
|
}
|
|
}
|
|
|
|
internal void ProcessUriRequest(PlayniteUriEventArgs args)
|
|
{
|
|
var arguments = args.Arguments;
|
|
if (args.Arguments.Count() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var command = arguments[0];
|
|
switch (command)
|
|
{
|
|
case UriCommands.CreateDiag:
|
|
CrashHandlerViewModel.CreateDiagPackage(Dialogs);
|
|
break;
|
|
|
|
case UriCommands.StartGame:
|
|
if (arguments.Count() != 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Guid.TryParse(arguments[1], out var gameId))
|
|
{
|
|
var game = Database.Games[gameId];
|
|
if (game == null)
|
|
{
|
|
logger.Error($"Cannot start game, game {arguments[1]} not found.");
|
|
}
|
|
else
|
|
{
|
|
GamesEditor.PlayGame(game, false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logger.Error($"Can't start game, failed to parse game id: {arguments[1]}");
|
|
}
|
|
|
|
break;
|
|
|
|
case UriCommands.InstallAddon:
|
|
if (arguments.Count() != 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
InstallOnlineAddon(arguments[1]);
|
|
break;
|
|
|
|
case UriCommands.Search:
|
|
if (Mode == ApplicationMode.Desktop)
|
|
{
|
|
PlayniteApiGlobal.MainView.OpenSearch(arguments.Length >= 2 ? arguments[1] : string.Empty);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
AppUriHandler(args);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void Quit(bool saveSettings = true)
|
|
{
|
|
logger.Info("Shutting down Playnite");
|
|
if (saveSettings)
|
|
{
|
|
AppSettings?.SaveSettings();
|
|
}
|
|
|
|
ReleaseResources();
|
|
CurrentNative.Shutdown(0);
|
|
}
|
|
|
|
public void QuitAndStart(string path, string arguments, bool asAdmin = false, bool saveSettings = true)
|
|
{
|
|
logger.Info("Shutting down Playnite and starting an app.");
|
|
if (saveSettings)
|
|
{
|
|
AppSettings?.SaveSettings();
|
|
}
|
|
|
|
ReleaseResources();
|
|
|
|
try
|
|
{
|
|
ProcessStarter.StartProcess(path, arguments, asAdmin);
|
|
}
|
|
catch (Exception e) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
// Not sure how this can happen, but there are some "operation cancelled by user" crashes here.
|
|
// People probably running Playnite as admin and cancelling UAC for new process, or something...
|
|
logger.Error(e, "Failed to start process on app shutdown.");
|
|
}
|
|
|
|
CurrentNative.Shutdown(0);
|
|
}
|
|
|
|
public void QuitAndExecute(Action action, bool saveSettings = true)
|
|
{
|
|
logger.Info("Shutting down Playnite and executing an action.");
|
|
if (saveSettings)
|
|
{
|
|
AppSettings?.SaveSettings();
|
|
}
|
|
|
|
ReleaseResources();
|
|
|
|
try
|
|
{
|
|
action();
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
logger.Error(e, "Failed to execute app quit action.");
|
|
}
|
|
|
|
CurrentNative.Shutdown(0);
|
|
}
|
|
|
|
public abstract void Restart(bool saveSettings = true);
|
|
|
|
public abstract void Restart(CmdLineOptions options, bool saveSettings = true);
|
|
|
|
public virtual void ReleaseResources(bool releaseCefSharp = true)
|
|
{
|
|
if (ResourcesReleased)
|
|
{
|
|
return;
|
|
}
|
|
|
|
logger.Debug("Releasing Playnite resources...");
|
|
CurrentNative.Dispatcher.Invoke(() =>
|
|
{
|
|
try
|
|
{
|
|
appMutex?.ReleaseMutex();
|
|
}
|
|
catch (Exception e) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
// Only happens when trying to release mutext created by a different process.
|
|
// This shouldn't normally happen since the mutex is released here before starting another instance.
|
|
logger.Error(e, "Failed to release app mutext.");
|
|
}
|
|
});
|
|
|
|
try
|
|
{
|
|
pipeServer?.StopServer();
|
|
}
|
|
catch (Exception e) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
// I have no idea why this fails for some people.
|
|
logger.Error(e, "Failed to stop pipe server.");
|
|
}
|
|
|
|
// Rare crash report of DiscordRPC not being loaded properly and then crashing on this
|
|
try
|
|
{
|
|
Discord?.Dispose();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error(e, "Failed to dispose Discord RPC.");
|
|
}
|
|
|
|
updateCheckTimer?.Dispose();
|
|
MainModelBase?.RunShutdowScript();
|
|
Extensions?.NotifiyOnApplicationStopped();
|
|
Dialogs.ActivateGlobalProgress(_ =>
|
|
{
|
|
try
|
|
{
|
|
if (GlobalTaskHandler.CancelAndWait(Common.Timer.SecondsToMilliseconds(5)) == false)
|
|
{
|
|
logger.Warn("Global task cancelation failed in time.");
|
|
}
|
|
|
|
GamesEditor?.Dispose();
|
|
Controllers?.Dispose();
|
|
Extensions?.Dispose();
|
|
}
|
|
catch (Exception exc) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(exc, "Failed to dispose Playnite objects.");
|
|
}
|
|
}, new GlobalProgressOptions("LOCClosingPlaynite"));
|
|
|
|
// This must run on main thread
|
|
if (releaseCefSharp)
|
|
{
|
|
CurrentNative.Dispatcher.Invoke(() =>
|
|
{
|
|
if (CefTools.IsInitialized)
|
|
{
|
|
CefTools.Shutdown();
|
|
}
|
|
});
|
|
}
|
|
|
|
Database?.Dispose();
|
|
ResourcesReleased = true;
|
|
}
|
|
|
|
private void CheckAddonBlacklist()
|
|
{
|
|
try
|
|
{
|
|
var manifests = ExtensionFactory.GetInstalledManifests();
|
|
var blackList = ServicesClient.GetAddonBlacklist();
|
|
var installedList = manifests.Where(a => blackList.Contains(a.Id)).ToList();
|
|
if (installedList.HasItems())
|
|
{
|
|
Dialogs.ShowMessage(ResourceProvider.GetString(LOC.WarningBlacklistedExtensions).Format(
|
|
string.Join(Environment.NewLine, installedList.Select(a => a.Name))),
|
|
"", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
}
|
|
catch (Exception exc)
|
|
{
|
|
logger.Warn(exc, "Failed to process addon blacklist check.");
|
|
}
|
|
}
|
|
|
|
private void CheckForUpdates(bool checkProgram, bool checkAddons)
|
|
{
|
|
if (checkProgram)
|
|
{
|
|
try
|
|
{
|
|
var showNotification = false;
|
|
var updater = new Updater(this);
|
|
if (updater.IsUpdateAvailable)
|
|
{
|
|
if (AppSettings.UpdateNotificationOnPatchesOnly)
|
|
{
|
|
showNotification = Updater.CurrentVersion.Major == updater.GetLatestVersion().Major;
|
|
}
|
|
else
|
|
{
|
|
showNotification = true;
|
|
}
|
|
}
|
|
|
|
if (showNotification)
|
|
{
|
|
var updateTitle = ResourceProvider.GetString("LOCUpdaterWindowTitle");
|
|
var updateBody = ResourceProvider.GetString("LOCUpdateIsAvailableNotificationBody");
|
|
if (!Current.IsActive)
|
|
{
|
|
ShowWindowsNotification(updateTitle, updateBody, () =>
|
|
{
|
|
Restore();
|
|
new UpdateViewModel(
|
|
updater,
|
|
new UpdateWindowFactory(),
|
|
new ResourceProvider(),
|
|
Dialogs,
|
|
Mode).OpenView();
|
|
});
|
|
}
|
|
|
|
MainModelBase.UpdatesAvailable = true;
|
|
}
|
|
}
|
|
catch (Exception exc)
|
|
{
|
|
logger.Warn(exc, "Failed to process program update.");
|
|
}
|
|
|
|
AppSettings.LastProgramUpdateCheck = DateTimes.Now;
|
|
}
|
|
|
|
if (checkAddons)
|
|
{
|
|
try
|
|
{
|
|
var updates = Addons.CheckAddonUpdates(ServicesClient);
|
|
if (updates.HasItems())
|
|
{
|
|
Notifications.Add(MainModelBase.GetAddonUpdatesFoundMessage(updates));
|
|
}
|
|
}
|
|
catch (Exception exc)
|
|
{
|
|
logger.Warn(exc, "Failed to process addon update check.");
|
|
}
|
|
|
|
AppSettings.LastAddonUpdateCheck = DateTimes.Now;
|
|
}
|
|
}
|
|
|
|
private void UpdateCheckerCallback(object state)
|
|
{
|
|
CheckForUpdates(AppSettings.ShouldCheckProgramUpdatePeriodic(), AppSettings.ShouldCheckAddonUpdatePeriodic());
|
|
CheckAddonBlacklist();
|
|
}
|
|
|
|
public async Task StartUpdateCheckerAsync()
|
|
{
|
|
if (PlayniteEnvironment.InOfflineMode)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await Task.Delay(Common.Timer.SecondsToMilliseconds(5));
|
|
if (GlobalTaskHandler.IsActive)
|
|
{
|
|
await GlobalTaskHandler.ProgressTask;
|
|
}
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
CheckForUpdates(AppSettings.ShouldCheckProgramUpdateStartup(), AppSettings.ShouldCheckAddonUpdateStartup());
|
|
CheckAddonBlacklist();
|
|
});
|
|
|
|
updateCheckTimer = new System.Threading.Timer(
|
|
UpdateCheckerCallback,
|
|
null,
|
|
Common.Timer.HoursToMilliseconds(4),
|
|
Common.Timer.HoursToMilliseconds(4));
|
|
}
|
|
|
|
public bool MigrateDatabase()
|
|
{
|
|
if (GameDatabase.GetMigrationRequired(AppSettings.DatabasePath))
|
|
{
|
|
var migrationProgress = new ProgressViewViewModel(
|
|
new ProgressWindowFactory(),
|
|
new GlobalProgressOptions(LOC.DBUpgradeProgress));
|
|
|
|
if (migrationProgress.ActivateProgress(
|
|
_ => GameDatabase.MigrateNewDatabaseFormat(GameDatabase.GetFullDbPath(AppSettings.DatabasePath))).Result != true)
|
|
{
|
|
logger.Error(migrationProgress.FailException, "Failed to migrate database to new version.");
|
|
var message = ResourceProvider.GetString("LOCDBUpgradeFail");
|
|
if (migrationProgress.FailException is NoDiskSpaceException exc)
|
|
{
|
|
message = string.Format(ResourceProvider.GetString("LOCDBUpgradeEmptySpaceFail"), Units.BytesToMegaBytes(exc.RequiredSpace));
|
|
}
|
|
|
|
Dialogs.ShowErrorMessage(message, "");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public void UpdateScreenInformation(Controls.WindowBase window)
|
|
{
|
|
try
|
|
{
|
|
DpiScale = VisualTreeHelper.GetDpi(window);
|
|
CurrentScreen = window.GetScreen();
|
|
}
|
|
catch (Exception e) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
DpiScale = new DpiScale(1, 1);
|
|
CurrentScreen = Computer.GetPrimaryScreen();
|
|
logger.Error(e, $"Failed to get window information for main {Mode} window.");
|
|
}
|
|
}
|
|
|
|
public void ShowAddonPerfNotice()
|
|
{
|
|
if (AppSettings.AddonsPerfNoticeShown)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Dialogs.ShowMessage(LOC.AddonPerfNotice, "", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
AppSettings.AddonsPerfNoticeShown = true;
|
|
AppSettings.SaveSettings();
|
|
}
|
|
|
|
public void InstallOnlineAddon(string addonId)
|
|
{
|
|
try
|
|
{
|
|
var addon = ServicesClient.GetAddon(addonId);
|
|
var package = addon.InstallerManifest.GetLatestCompatiblePackage();
|
|
if (package == null)
|
|
{
|
|
Dialogs.ShowErrorMessage(LOC.AddonErrorNotCompatible, "");
|
|
return;
|
|
}
|
|
|
|
var message = string.Format(
|
|
ResourceProvider.GetString(addon.IsTheme ? LOC.ThemeInstallPrompt : LOC.ExtensionInstallPrompt),
|
|
addon.Name, addon.Author, package.Version);
|
|
BaseExtensionManifest existing = null;
|
|
if (addon.IsTheme)
|
|
{
|
|
existing = ThemeManager.GetAvailableThemes().FirstOrDefault(a => a.Id == addon.AddonId);
|
|
}
|
|
else
|
|
{
|
|
existing = ExtensionFactory.GetInstalledManifests().FirstOrDefault(a => a.Id == addon.AddonId);
|
|
}
|
|
|
|
if (existing != null)
|
|
{
|
|
message = string.Format(
|
|
ResourceProvider.GetString(addon.IsTheme ? LOC.ThemeUpdatePrompt : LOC.ExtensionUpdatePrompt),
|
|
addon.Name, existing.Version, package.Version);
|
|
}
|
|
|
|
if (Dialogs.ShowMessage(message, LOC.GeneralExtensionInstallTitle, MessageBoxButton.YesNo) != MessageBoxResult.Yes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var licenseRes = addon.CheckAddonLicense();
|
|
if (licenseRes == null)
|
|
{
|
|
Dialogs.ShowErrorMessage(LOC.AddonErrorDownloadFailed, string.Empty);
|
|
return;
|
|
}
|
|
|
|
if (licenseRes == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ShowAddonPerfNotice();
|
|
var locaPath = addon.GetTargetDownloadPath();
|
|
FileSystem.DeleteFile(locaPath);
|
|
var res = Dialogs.ActivateGlobalProgress((_) =>
|
|
{
|
|
if (package.PackageUrl.IsHttpUrl())
|
|
{
|
|
FileSystem.PrepareSaveFile(locaPath);
|
|
HttpDownloader.DownloadFile(package.PackageUrl, locaPath);
|
|
}
|
|
else
|
|
{
|
|
File.Copy(package.PackageUrl, locaPath);
|
|
}
|
|
},
|
|
new GlobalProgressOptions(LOC.DownloadingLabel, false));
|
|
if (res.Error != null)
|
|
{
|
|
logger.Error(res.Error, $"Failed to download addon {package.PackageUrl}");
|
|
Dialogs.ShowErrorMessage(LOC.AddonErrorDownloadFailed, string.Empty);
|
|
return;
|
|
}
|
|
|
|
ExtensionInstaller.QueuePackageInstall(locaPath);
|
|
if (Dialogs.ShowMessage(LOC.ExtInstallationRestartNotif, LOC.SettingsRestartTitle,
|
|
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
|
{
|
|
Restart(new CmdLineOptions { SkipLibUpdate = true });
|
|
};
|
|
}
|
|
catch (Exception e) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(e, $"Failed to install addon from uri {addonId}");
|
|
}
|
|
}
|
|
|
|
public void InstallThemeFile(string themeFile)
|
|
{
|
|
try
|
|
{
|
|
ExtensionInstaller.VerifyThemePackage(themeFile);
|
|
var desc = ExtensionInstaller.GetPackedThemeManifest(themeFile);
|
|
desc.VerifyManifest();
|
|
|
|
if (new Version(desc.ThemeApiVersion).Major != ThemeManager.GetApiVersion(desc.Mode).Major)
|
|
{
|
|
throw new Exception(ResourceProvider.GetString("LOCGeneralExtensionInstallApiVersionFails"));
|
|
}
|
|
|
|
var message = string.Format(ResourceProvider.GetString("LOCThemeInstallPrompt"),
|
|
desc.Name, desc.Author, desc.Version);
|
|
var existing = ThemeManager.GetAvailableThemes(desc.Mode).FirstOrDefault(a => a.Id == desc.Id);
|
|
if (existing != null)
|
|
{
|
|
message = string.Format(ResourceProvider.GetString("LOCThemeUpdatePrompt"),
|
|
desc.Name, existing.Version, desc.Version);
|
|
}
|
|
|
|
if (Dialogs.ShowMessage(
|
|
message,
|
|
ResourceProvider.GetString("LOCGeneralExtensionInstallTitle"),
|
|
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
|
{
|
|
ShowAddonPerfNotice();
|
|
ExtensionInstaller.QueuePackageInstall(themeFile);
|
|
if (Dialogs.ShowMessage(
|
|
ResourceProvider.GetString("LOCExtInstallationRestartNotif"),
|
|
ResourceProvider.GetString("LOCSettingsRestartTitle"),
|
|
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
|
{
|
|
Restart(new CmdLineOptions()
|
|
{
|
|
SkipLibUpdate = true,
|
|
});
|
|
};
|
|
}
|
|
}
|
|
catch (Exception e) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(e, "Failed to install theme.");
|
|
Dialogs.ShowErrorMessage(
|
|
string.Format(ResourceProvider.GetString("LOCThemeInstallFail"), e.Message), "");
|
|
}
|
|
}
|
|
|
|
public void InstallExtensionFile(string extensionFile)
|
|
{
|
|
try
|
|
{
|
|
ExtensionInstaller.VerifyExtensionPackage(extensionFile);
|
|
var desc = ExtensionInstaller.GetPackedExtensionManifest(extensionFile);
|
|
desc.VerifyManifest();
|
|
|
|
var message = string.Format(ResourceProvider.GetString("LOCExtensionInstallPrompt"),
|
|
desc.Name, desc.Author, desc.Version);
|
|
var existing = ExtensionFactory.GetInstalledManifests().FirstOrDefault(a => a.Id == desc.Id);
|
|
if (existing != null)
|
|
{
|
|
message = string.Format(ResourceProvider.GetString("LOCExtensionUpdatePrompt"),
|
|
desc.Name, existing.Version, desc.Version);
|
|
}
|
|
|
|
if (Dialogs.ShowMessage(
|
|
message,
|
|
ResourceProvider.GetString("LOCGeneralExtensionInstallTitle"),
|
|
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
|
{
|
|
ShowAddonPerfNotice();
|
|
ExtensionInstaller.QueuePackageInstall(extensionFile);
|
|
if (Dialogs.ShowMessage(
|
|
ResourceProvider.GetString("LOCExtInstallationRestartNotif"),
|
|
ResourceProvider.GetString("LOCSettingsRestartTitle"),
|
|
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
|
{
|
|
Restart(new CmdLineOptions()
|
|
{
|
|
SkipLibUpdate = true,
|
|
});
|
|
};
|
|
}
|
|
}
|
|
catch (Exception e) when (!PlayniteEnvironment.ThrowAllErrors)
|
|
{
|
|
logger.Error(e, "Failed to install extension.");
|
|
Dialogs.ShowErrorMessage(
|
|
string.Format(ResourceProvider.GetString("LOCExtensionInstallFail"), e.Message), "");
|
|
}
|
|
}
|
|
|
|
public void OnExtensionsLoaded()
|
|
{
|
|
ExtensionsLoaded?.Invoke(this, EventArgs.Empty);
|
|
OnPropertyChanged(nameof(this.ExtensionsStatusBinder));
|
|
}
|
|
|
|
private void WaitForOtherInstacesToExit(bool throwOnTimetout)
|
|
{
|
|
if (Process.GetProcesses().Where(a => IsProcessPlayniteProcess(a)).Count() > 1)
|
|
{
|
|
logger.Info("Multiple Playnite instances detected, waiting for them to close.");
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
Thread.Sleep(500);
|
|
if (Process.GetProcesses().Where(a => IsProcessPlayniteProcess(a)).Count() == 1)
|
|
{
|
|
break;
|
|
}
|
|
else if (i == 9)
|
|
{
|
|
if (throwOnTimetout)
|
|
{
|
|
throw new Exception("Another Playnite instance didn't shutdown in time.");
|
|
}
|
|
else
|
|
{
|
|
logger.Warn("Another Playnite instance didn't shutdown in time.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static bool IsProcessPlayniteProcess(Process process)
|
|
{
|
|
return process.ProcessName.StartsWith("Playnite.DesktopApp") || process.ProcessName.StartsWith("Playnite.FullscreenApp");
|
|
}
|
|
|
|
public abstract PlayniteAPI GetApiInstance(ExtensionManifest pluginOwner);
|
|
public abstract PlayniteAPI GetApiInstance();
|
|
}
|
|
} |