mirror of
https://git.naxdy.org/Mirror/Ryujinx.git
synced 2026-01-12 02:00:25 +00:00
Merge branch '20-empty-nca-lockup' into 'master'
UI: LoadGuestApplication asynchronous cancellation See merge request [ryubing/ryujinx!240](https://git.ryujinx.app/ryubing/ryujinx/-/merge_requests/240)
This commit is contained in:
commit
f61d60df2f
9 changed files with 131 additions and 57 deletions
|
|
@ -1,3 +1,4 @@
|
|||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
|
@ -114,7 +115,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
cbs.AddDependant(this);
|
||||
|
||||
// We need to add a dependency on the command buffer to all objects this object
|
||||
// references aswell.
|
||||
// references as well.
|
||||
if (_referencedObjs != null)
|
||||
{
|
||||
for (int i = 0; i < _referencedObjs.Length; i++)
|
||||
|
|
@ -176,6 +177,8 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
}
|
||||
}
|
||||
|
||||
// This can somehow become -1.
|
||||
// Logger.Info?.PrintMsg(LogClass.Gpu, $"_referenceCount: {_referenceCount}");
|
||||
Debug.Assert(_referenceCount >= 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1059,7 +1059,7 @@ namespace Ryujinx.HLE.FileSystem
|
|||
}
|
||||
}
|
||||
|
||||
public static bool AreKeysAlredyPresent(string pathToCheck)
|
||||
public static bool AreKeysAlreadyPresent(string pathToCheck)
|
||||
{
|
||||
string[] fileNames = ["prod.keys", "title.keys", "console.keys", "dev.keys"];
|
||||
foreach (string file in fileNames)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ using Ryujinx.HLE.Loaders.Executables;
|
|||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
|
|
@ -27,10 +28,15 @@ namespace Ryujinx.HLE.Loaders.Processes
|
|||
|
||||
private ulong _latestPid;
|
||||
|
||||
public ProcessResult ActiveApplication
|
||||
public ProcessResult? ActiveApplication
|
||||
{
|
||||
get
|
||||
{
|
||||
return _processesByPid.GetValueOrDefault(_latestPid);
|
||||
|
||||
// Using this if statement locks up the UI and prevents a new game from loading.
|
||||
// Haven't quite deduced why yet.
|
||||
|
||||
if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value))
|
||||
throw new RyujinxException(
|
||||
$"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?");
|
||||
|
|
@ -144,7 +150,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
|||
public bool LoadUnpackedNca(string exeFsDirPath, string romFsPath = null)
|
||||
{
|
||||
ProcessResult processResult = new LocalFileSystem(exeFsDirPath).Load(_device, romFsPath);
|
||||
|
||||
|
||||
if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult))
|
||||
{
|
||||
if (processResult.Start(_device))
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ using LibHac.Ns;
|
|||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Cpu;
|
||||
using Ryujinx.HLE.HOS.SystemState;
|
||||
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||
using Ryujinx.Horizon.Common;
|
||||
|
||||
namespace Ryujinx.HLE.Loaders.Processes
|
||||
|
|
@ -52,6 +51,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
|||
|
||||
if (metaLoader is not null)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application,$"metaLoader: {metaLoader}");
|
||||
ulong programId = metaLoader.ProgramId;
|
||||
|
||||
Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString();
|
||||
|
|
@ -71,8 +71,15 @@ namespace Ryujinx.HLE.Loaders.Processes
|
|||
ProgramId = programId;
|
||||
ProgramIdText = $"{programId:x16}";
|
||||
Is64Bit = metaLoader.IsProgram64Bit;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application,$"metaLoader is null !!!");
|
||||
ProcessId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
DiskCacheEnabled = diskCacheEnabled;
|
||||
AllowCodeMemoryForJit = allowCodeMemoryForJit;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ using VSyncMode = Ryujinx.Common.Configuration.VSyncMode;
|
|||
|
||||
namespace Ryujinx.Ava.Systems
|
||||
{
|
||||
internal class AppHost
|
||||
internal class AppHost : IDisposable
|
||||
{
|
||||
private const int CursorHideIdleTime = 5; // Hide Cursor seconds.
|
||||
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
|
||||
|
|
@ -437,7 +437,7 @@ namespace Ryujinx.Ava.Systems
|
|||
|
||||
SaveBitmapAsPng(bitmapToSave, path);
|
||||
|
||||
Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot");
|
||||
Logger.Notice.Print(LogClass.Application, $"Screenshot saved to '{path}'.", "Screenshot");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -610,27 +610,40 @@ namespace Ryujinx.Ava.Systems
|
|||
|
||||
_isActive = false;
|
||||
|
||||
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
|
||||
// We only need to wait for all commands submitted during the main gpu loop to be processed.
|
||||
_gpuDoneEvent.WaitOne();
|
||||
_gpuDoneEvent.Dispose();
|
||||
|
||||
DisplaySleep.Restore();
|
||||
|
||||
NpadManager.Dispose();
|
||||
TouchScreenManager.Dispose();
|
||||
Device.Dispose();
|
||||
|
||||
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
|
||||
// We only need to wait for all commands submitted during the main gpu loop to be processed.
|
||||
// If the GPU has no work and is cancelled, we need to handle that as well.
|
||||
|
||||
WaitHandle.WaitAny(new[] { _gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle });
|
||||
_gpuCancellationTokenSource.Dispose();
|
||||
|
||||
// Waiting for work to be finished before we dispose.
|
||||
if (_renderingStarted)
|
||||
{
|
||||
Device.Gpu.WaitUntilGpuReady();
|
||||
}
|
||||
|
||||
_gpuDoneEvent.Dispose();
|
||||
|
||||
DisposeGpu();
|
||||
|
||||
AppExit?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void Dispose()
|
||||
// MUST be public to inherit from IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
if (Device.Processes != null)
|
||||
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _playTimer.Elapsed);
|
||||
|
||||
{
|
||||
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication?.ProgramIdText,
|
||||
_playTimer.Elapsed);
|
||||
}
|
||||
|
||||
ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
|
||||
ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
|
||||
|
|
@ -645,7 +658,6 @@ namespace Ryujinx.Ava.Systems
|
|||
_topLevel.PointerExited -= TopLevel_PointerExited;
|
||||
|
||||
_gpuCancellationTokenSource.Cancel();
|
||||
_gpuCancellationTokenSource.Dispose();
|
||||
|
||||
_chrono.Stop();
|
||||
_playTimer.Stop();
|
||||
|
|
@ -671,6 +683,12 @@ namespace Ryujinx.Ava.Systems
|
|||
}
|
||||
else
|
||||
{
|
||||
// No use waiting on something that never started work
|
||||
if (_renderingStarted)
|
||||
{
|
||||
Device.Gpu.WaitUntilGpuReady();
|
||||
}
|
||||
|
||||
Device.DisposeGpu();
|
||||
}
|
||||
}
|
||||
|
|
@ -685,7 +703,7 @@ namespace Ryujinx.Ava.Systems
|
|||
_cursorState = CursorStates.ForceChangeCursor;
|
||||
}
|
||||
|
||||
public async Task<bool> LoadGuestApplication(BlitStruct<ApplicationControlProperty>? customNacpData = null)
|
||||
public async Task LoadGuestApplication(CancellationTokenSource cts, BlitStruct<ApplicationControlProperty>? customNacpData = null)
|
||||
{
|
||||
DiscordIntegrationModule.GuestAppStartedAt = Timestamps.Now;
|
||||
|
||||
|
|
@ -714,7 +732,8 @@ namespace Ryujinx.Ava.Systems
|
|||
await UserErrorDialog.ShowUserErrorDialog(userError);
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -723,10 +742,11 @@ namespace Ryujinx.Ava.Systems
|
|||
await UserErrorDialog.ShowUserErrorDialog(userError);
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
|
||||
// Tell the user that we installed a firmware for them.
|
||||
// Tell the user that we installed firmware for them.
|
||||
if (userError is UserError.NoFirmware)
|
||||
{
|
||||
firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
|
||||
|
|
@ -746,7 +766,8 @@ namespace Ryujinx.Ava.Systems
|
|||
await UserErrorDialog.ShowUserErrorDialog(userError);
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -761,7 +782,8 @@ namespace Ryujinx.Ava.Systems
|
|||
{
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
}
|
||||
else if (Directory.Exists(ApplicationPath))
|
||||
|
|
@ -781,20 +803,24 @@ namespace Ryujinx.Ava.Systems
|
|||
|
||||
if (!Device.LoadCart(ApplicationPath, romFsFiles[0]))
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(
|
||||
"Please specify an unpacked game directory with a valid exefs or NSO/NRO.");
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
|
||||
|
||||
if (!Device.LoadCart(ApplicationPath))
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(
|
||||
"Please specify an unpacked game directory with a valid exefs or NSO/NRO.");
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -812,7 +838,8 @@ namespace Ryujinx.Ava.Systems
|
|||
{
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -825,7 +852,8 @@ namespace Ryujinx.Ava.Systems
|
|||
{
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -839,7 +867,8 @@ namespace Ryujinx.Ava.Systems
|
|||
{
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -854,7 +883,8 @@ namespace Ryujinx.Ava.Systems
|
|||
{
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
|
|
@ -863,7 +893,8 @@ namespace Ryujinx.Ava.Systems
|
|||
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -872,19 +903,18 @@ namespace Ryujinx.Ava.Systems
|
|||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
|
||||
Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NSO/NRO file.");
|
||||
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
cts.Cancel();
|
||||
throw new OperationCanceledException(cts.Token);
|
||||
}
|
||||
|
||||
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText,
|
||||
appMetadata => appMetadata.UpdatePreGame()
|
||||
);
|
||||
_playTimer.Start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void Resume()
|
||||
|
|
@ -894,7 +924,7 @@ namespace Ryujinx.Ava.Systems
|
|||
_viewModel.IsPaused = false;
|
||||
_playTimer.Start();
|
||||
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI);
|
||||
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
|
||||
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed.");
|
||||
}
|
||||
|
||||
internal void Pause()
|
||||
|
|
@ -904,7 +934,7 @@ namespace Ryujinx.Ava.Systems
|
|||
_viewModel.IsPaused = true;
|
||||
_playTimer.Stop();
|
||||
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]);
|
||||
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
|
||||
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused.");
|
||||
}
|
||||
|
||||
private void InitEmulatedSwitch()
|
||||
|
|
@ -1097,7 +1127,9 @@ namespace Ryujinx.Ava.Systems
|
|||
// Make sure all commands in the run loop are fully executed before leaving the loop.
|
||||
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
|
||||
{
|
||||
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushing threaded commands...");
|
||||
threaded.FlushThreadedCommands();
|
||||
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!");
|
||||
}
|
||||
|
||||
_gpuDoneEvent.Set();
|
||||
|
|
|
|||
|
|
@ -849,7 +849,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary
|
|||
|
||||
foreach (ApplicationData installedApplication in Applications.Items)
|
||||
{
|
||||
temporary += LoadAndSaveMetaData(installedApplication.IdString).TimePlayed;
|
||||
// this should always exist... should...
|
||||
temporary += LoadAndSaveMetaData(installedApplication.IdString).Value.TimePlayed;
|
||||
}
|
||||
|
||||
TotalTimePlayed = temporary;
|
||||
|
|
@ -1159,15 +1160,22 @@ namespace Ryujinx.Ava.Systems.AppLibrary
|
|||
ApplicationCountUpdated?.Invoke(null, e);
|
||||
}
|
||||
|
||||
public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action<ApplicationMetadata> modifyFunction = null)
|
||||
public static Gommon.Optional<ApplicationMetadata> LoadAndSaveMetaData(string titleId, Action<ApplicationMetadata> modifyFunction = null)
|
||||
{
|
||||
if (titleId is null)
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.Application, "Cannot save metadata because title ID is invalid.");
|
||||
return null;
|
||||
}
|
||||
|
||||
string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui");
|
||||
string metadataFile = Path.Combine(metadataFolder, "metadata.json");
|
||||
|
||||
ApplicationMetadata appMetadata;
|
||||
|
||||
|
||||
if (!File.Exists(metadataFile))
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, $"Metadata file does not exist. Creating metadata for {titleId}...");
|
||||
Directory.CreateDirectory(metadataFolder);
|
||||
|
||||
appMetadata = new ApplicationMetadata();
|
||||
|
|
@ -1177,12 +1185,12 @@ namespace Ryujinx.Ava.Systems.AppLibrary
|
|||
|
||||
try
|
||||
{
|
||||
Logger.Debug?.Print(LogClass.Application, $"Deserializing metadata for {titleId}...");
|
||||
appMetadata = JsonHelper.DeserializeFromFile(metadataFile, _serializerContext.ApplicationMetadata);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults.");
|
||||
|
||||
appMetadata = new ApplicationMetadata();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ namespace Ryujinx.Ava.Systems
|
|||
|
||||
public static void Use(Optional<string> titleId)
|
||||
{
|
||||
if (titleId.TryGet(out string tid))
|
||||
if (titleId.TryGet(out string tid) && Switch.Shared.Processes.ActiveApplication is not null)
|
||||
SwitchToPlayingState(
|
||||
ApplicationLibrary.LoadAndSaveMetaData(tid),
|
||||
Switch.Shared.Processes.ActiveApplication
|
||||
|
|
|
|||
|
|
@ -57,8 +57,15 @@ namespace Ryujinx.Ava.UI.Models
|
|||
}
|
||||
else
|
||||
{
|
||||
ApplicationMetadata appMetadata = ApplicationLibrary.LoadAndSaveMetaData(TitleIdString);
|
||||
Title = appMetadata.Title ?? TitleIdString;
|
||||
Gommon.Optional<ApplicationMetadata> appMetadata = ApplicationLibrary.LoadAndSaveMetaData(TitleIdString);
|
||||
if (appMetadata != null)
|
||||
{
|
||||
Title = appMetadata.Value.Title ?? TitleIdString;
|
||||
}
|
||||
else
|
||||
{
|
||||
Title = "<INVALID>";
|
||||
}
|
||||
}
|
||||
|
||||
Task.Run(() =>
|
||||
|
|
|
|||
|
|
@ -1029,7 +1029,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
string dialogMessage =
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallMessage);
|
||||
|
||||
if (ContentManager.AreKeysAlredyPresent(systemDirectory))
|
||||
if (ContentManager.AreKeysAlreadyPresent(systemDirectory))
|
||||
{
|
||||
dialogMessage +=
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys
|
||||
|
|
@ -1703,11 +1703,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
|
||||
Logger.RestartTime();
|
||||
|
||||
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path,
|
||||
ConfigurationState.Instance.System.Language, application.Id);
|
||||
|
||||
PrepareLoadScreen();
|
||||
|
||||
RendererHostControl = new RendererHost();
|
||||
|
||||
AppHost = new AppHost(
|
||||
|
|
@ -1721,18 +1716,34 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
UserChannelPersistence,
|
||||
this,
|
||||
TopLevel);
|
||||
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
|
||||
if (!await AppHost.LoadGuestApplication(customNacpData))
|
||||
try
|
||||
{
|
||||
await AppHost.LoadGuestApplication(cts, customNacpData);
|
||||
}
|
||||
catch (OperationCanceledException exception)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application,
|
||||
"LoadGuestApplication was interrupted !!! " + exception.Message);
|
||||
AppHost.DisposeContext();
|
||||
AppHost = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
finally
|
||||
{
|
||||
cts.Dispose();
|
||||
}
|
||||
|
||||
CanUpdate = false;
|
||||
|
||||
application.Name ??= AppHost.Device.Processes.ActiveApplication.Name;
|
||||
|
||||
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path,
|
||||
ConfigurationState.Instance.System.Language, application.Id);
|
||||
|
||||
PrepareLoadScreen();
|
||||
|
||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, application.Name);
|
||||
|
||||
|
|
@ -1754,9 +1765,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||
RendererHostControl.Focus();
|
||||
});
|
||||
|
||||
public static void UpdateGameMetadata(string titleId, TimeSpan playTime)
|
||||
public static void UpdateGameMetadata(string titleId, TimeSpan playTime)
|
||||
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime));
|
||||
|
||||
|
||||
public void RefreshFirmwareStatus()
|
||||
{
|
||||
SystemVersion version = null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue