mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efdfa68dab | ||
|
|
87110f1d69 |
17
.github/FIX_REPORT.md
vendored
17
.github/FIX_REPORT.md
vendored
@@ -8,14 +8,14 @@ MSBUILD : error MSB1003: Specify a project or solution file.
|
|||||||
The current working directory does not contain a project or solution file.
|
The current working directory does not contain a project or solution file.
|
||||||
```
|
```
|
||||||
|
|
||||||
**原因**: 项目中缺少 `LanMountainDesktop.sln` 解决方案文件,但工作流尝试执行 `dotnet restore` 而没有指定项目。
|
**原因**: 项目中缺少 `LanMountainDesktop.slnx` 解决方案文件,但工作流尝试执行 `dotnet restore` 而没有指定项目。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔧 已采取的修复
|
## 🔧 已采取的修复
|
||||||
|
|
||||||
### 1. 创建解决方案文件
|
### 1. 创建 `.slnx` 解决方案文件
|
||||||
✅ 创建了标准的 `LanMountainDesktop.sln` 文件,包含:
|
✅ 创建了标准的 `LanMountainDesktop.slnx` 文件,包含:
|
||||||
- `LanMountainDesktop/LanMountainDesktop.csproj`
|
- `LanMountainDesktop/LanMountainDesktop.csproj`
|
||||||
|
|
||||||
### 2. 验证本地构建工作
|
### 2. 验证本地构建工作
|
||||||
@@ -35,10 +35,10 @@ The current working directory does not contain a project or solution file.
|
|||||||
|
|
||||||
## 📋 解决方案文件内容
|
## 📋 解决方案文件内容
|
||||||
|
|
||||||
包含主桌面项目的标准 Visual Studio 解决方案格式:
|
包含主桌面项目的标准 XML 解决方案格式:
|
||||||
|
|
||||||
```
|
```
|
||||||
LanMountainDesktop.sln
|
LanMountainDesktop.slnx
|
||||||
└── LanMountainDesktop (Desktop UI - Avalonia)
|
└── LanMountainDesktop (Desktop UI - Avalonia)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -50,10 +50,11 @@ LanMountainDesktop.sln
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 添加新创建的解决方案文件
|
# 1. 添加新创建的解决方案文件
|
||||||
git add LanMountainDesktop.sln
|
git add LanMountainDesktop.slnx
|
||||||
|
git add global.json
|
||||||
|
|
||||||
# 2. 提交
|
# 2. 提交
|
||||||
git commit -m "Add solution file for desktop project"
|
git commit -m "Migrate desktop solution to .slnx"
|
||||||
|
|
||||||
# 3. 推送
|
# 3. 推送
|
||||||
git push origin main
|
git push origin main
|
||||||
@@ -92,7 +93,7 @@ git push origin v1.0.1
|
|||||||
| `.github/workflows/code-quality.yml` | 代码质量检查 | ✅ 可用 |
|
| `.github/workflows/code-quality.yml` | 代码质量检查 | ✅ 可用 |
|
||||||
| `.github/workflows/release.yml` | 多平台发布 | ✅ 可用 |
|
| `.github/workflows/release.yml` | 多平台发布 | ✅ 可用 |
|
||||||
| `.github/workflows/issue-management.yml` | Issue自动管理 | ✅ 可用 |
|
| `.github/workflows/issue-management.yml` | Issue自动管理 | ✅ 可用 |
|
||||||
| `LanMountainDesktop.sln` | 解决方案文件 | ✅ 已修复 |
|
| `LanMountainDesktop.slnx` | 解决方案文件 | ✅ 已修复 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
3
.github/README.md
vendored
3
.github/README.md
vendored
@@ -36,9 +36,10 @@
|
|||||||
- 扩展契约与字段说明见组件系统文档:`LanMountainDesktop/ComponentSystem/README.md`。
|
- 扩展契约与字段说明见组件系统文档:`LanMountainDesktop/ComponentSystem/README.md`。
|
||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
- 项目包含桌面端与推荐后端两个子项目,并在同一 solution 中维护。
|
- 项目包含桌面端与推荐后端两个子项目,并在同一 `LanMountainDesktop.slnx` 工作区中维护。
|
||||||
- 配置默认写入本地:`%LOCALAPPDATA%\LanMountainDesktop\settings.json`。
|
- 配置默认写入本地:`%LOCALAPPDATA%\LanMountainDesktop\settings.json`。
|
||||||
- 当前体验以 Windows 为主要目标平台。
|
- 当前体验以 Windows 为主要目标平台。
|
||||||
|
- SDK 版本由仓库根目录 `global.json` 锁定。
|
||||||
|
|
||||||
## 运行说明
|
## 运行说明
|
||||||
运行与环境准备已拆分到独立文档:[`run.md`](./run.md)
|
运行与环境准备已拆分到独立文档:[`run.md`](./run.md)
|
||||||
|
|||||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
DOTNET_VERSION: '10.0.x'
|
DOTNET_VERSION: '10.0.x'
|
||||||
Solution_Name: LanMountainDesktop.sln
|
Solution_Name: LanMountainDesktop.slnx
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-windows:
|
build-windows:
|
||||||
|
|||||||
2
.github/workflows/code-quality.yml
vendored
2
.github/workflows/code-quality.yml
vendored
@@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
DOTNET_VERSION: '10.0.x'
|
DOTNET_VERSION: '10.0.x'
|
||||||
Solution_Name: LanMountainDesktop.sln
|
Solution_Name: LanMountainDesktop.slnx
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -18,7 +18,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
DOTNET_VERSION: '10.0.x'
|
DOTNET_VERSION: '10.0.x'
|
||||||
Solution_Name: LanMountainDesktop.sln
|
Solution_Name: LanMountainDesktop.slnx
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ using LanMountainDesktop.PluginSdk;
|
|||||||
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
|
private static readonly TimeSpan[] RetryDelays =
|
||||||
|
[
|
||||||
|
TimeSpan.FromMilliseconds(120),
|
||||||
|
TimeSpan.FromMilliseconds(250),
|
||||||
|
TimeSpan.FromMilliseconds(500)
|
||||||
|
];
|
||||||
|
|
||||||
private static async Task<int> Main(string[] args)
|
private static async Task<int> Main(string[] args)
|
||||||
{
|
{
|
||||||
var result = new HelperResult();
|
var result = new HelperResult();
|
||||||
@@ -35,10 +42,12 @@ internal static class Program
|
|||||||
|
|
||||||
var manifest = ReadManifestFromPackage(fullSourcePath);
|
var manifest = ReadManifestFromPackage(fullSourcePath);
|
||||||
Directory.CreateDirectory(fullPluginsDirectory);
|
Directory.CreateDirectory(fullPluginsDirectory);
|
||||||
RemoveExistingPluginPackages(fullPluginsDirectory, manifest.Id);
|
|
||||||
|
|
||||||
var destinationPath = Path.Combine(fullPluginsDirectory, BuildInstalledPackageFileName(manifest.Id));
|
var destinationPath = Path.Combine(fullPluginsDirectory, BuildInstalledPackageFileName(manifest.Id));
|
||||||
File.Copy(fullSourcePath, destinationPath, overwrite: true);
|
var stagingPath = destinationPath + ".incoming";
|
||||||
|
DeleteFileWithRetry(stagingPath);
|
||||||
|
CopyWithRetry(fullSourcePath, stagingPath, overwrite: true);
|
||||||
|
RemoveExistingPluginPackages(fullPluginsDirectory, manifest.Id, destinationPath, stagingPath);
|
||||||
|
MoveWithOverwriteRetry(stagingPath, destinationPath);
|
||||||
|
|
||||||
result = new HelperResult
|
result = new HelperResult
|
||||||
{
|
{
|
||||||
@@ -123,7 +132,7 @@ internal static class Program
|
|||||||
return PluginManifest.Load(stream, $"{packagePath}!/{entries[0].FullName}");
|
return PluginManifest.Load(stream, $"{packagePath}!/{entries[0].FullName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId)
|
private static void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId, string destinationPath, string stagingPath)
|
||||||
{
|
{
|
||||||
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), PluginSdkInfo.RuntimeDirectoryName));
|
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), PluginSdkInfo.RuntimeDirectoryName));
|
||||||
foreach (var existingPackagePath in Directory
|
foreach (var existingPackagePath in Directory
|
||||||
@@ -133,13 +142,19 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (string.Equals(existingPackagePath, Path.GetFullPath(destinationPath), StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(existingPackagePath, Path.GetFullPath(stagingPath), StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var existingManifest = ReadManifestFromPackage(existingPackagePath);
|
var existingManifest = ReadManifestFromPackage(existingPackagePath);
|
||||||
if (!string.Equals(existingManifest.Id, pluginId, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(existingManifest.Id, pluginId, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
File.Delete(existingPackagePath);
|
DeleteFileWithRetry(existingPackagePath);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -148,6 +163,56 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void CopyWithRetry(string sourcePath, string destinationPath, bool overwrite)
|
||||||
|
{
|
||||||
|
Retry(() => File.Copy(sourcePath, destinationPath, overwrite));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MoveWithOverwriteRetry(string sourcePath, string destinationPath)
|
||||||
|
{
|
||||||
|
Retry(() => File.Move(sourcePath, destinationPath, overwrite: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteFileWithRetry(string filePath)
|
||||||
|
{
|
||||||
|
Retry(() =>
|
||||||
|
{
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
{
|
||||||
|
File.Delete(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Retry(Action action)
|
||||||
|
{
|
||||||
|
Exception? lastException = null;
|
||||||
|
|
||||||
|
for (var attempt = 0; attempt <= RetryDelays.Length; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
if (attempt >= RetryDelays.Length)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(RetryDelays[attempt]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastException is not null)
|
||||||
|
{
|
||||||
|
throw lastException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildInstalledPackageFileName(string pluginId)
|
private static string BuildInstalledPackageFileName(string pluginId)
|
||||||
{
|
{
|
||||||
var invalidChars = Path.GetInvalidFileNameChars();
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.0.31903.59
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanMountainDesktop", "LanMountainDesktop\LanMountainDesktop.csproj", "{00000001-0000-0000-0000-000000000001}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanMountainDesktop.SamplePlugin", "LanAirApp\samples\LanMountainDesktop.SamplePlugin\LanMountainDesktop.SamplePlugin.csproj", "{BDCD028D-DB6E-4BD5-994A-65889DBDEE0C}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanMountainDesktop.PluginPackager", "LanAirApp\tools\LanMountainDesktop.PluginPackager\LanMountainDesktop.PluginPackager.csproj", "{AAE8578B-1F9D-4D4F-8B2E-0A98C55B0C31}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanMountainDesktop.PluginSdk", "LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj", "{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanMountainDesktop.PluginsInstallHelper", "LanMountainDesktop.PluginsInstallHelper\LanMountainDesktop.PluginsInstallHelper.csproj", "{5A20D5E9-BD27-4DD4-9F4F-97E0367DD2AC}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{00000001-0000-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{00000001-0000-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{00000001-0000-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{00000001-0000-0000-0000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{BDCD028D-DB6E-4BD5-994A-65889DBDEE0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{BDCD028D-DB6E-4BD5-994A-65889DBDEE0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{BDCD028D-DB6E-4BD5-994A-65889DBDEE0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{BDCD028D-DB6E-4BD5-994A-65889DBDEE0C}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{AAE8578B-1F9D-4D4F-8B2E-0A98C55B0C31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{AAE8578B-1F9D-4D4F-8B2E-0A98C55B0C31}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{AAE8578B-1F9D-4D4F-8B2E-0A98C55B0C31}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{AAE8578B-1F9D-4D4F-8B2E-0A98C55B0C31}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{30A0F689-AACC-48C8-8BFE-BC7BFBA6CC55}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{5A20D5E9-BD27-4DD4-9F4F-97E0367DD2AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{5A20D5E9-BD27-4DD4-9F4F-97E0367DD2AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{5A20D5E9-BD27-4DD4-9F4F-97E0367DD2AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{5A20D5E9-BD27-4DD4-9F4F-97E0367DD2AC}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
7
LanMountainDesktop.slnx
Normal file
7
LanMountainDesktop.slnx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="LanAirApp/samples/LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.csproj" />
|
||||||
|
<Project Path="LanAirApp/tools/LanMountainDesktop.PluginPackager/LanMountainDesktop.PluginPackager.csproj" />
|
||||||
|
<Project Path="LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj" />
|
||||||
|
<Project Path="LanMountainDesktop.PluginsInstallHelper/LanMountainDesktop.PluginsInstallHelper.csproj" />
|
||||||
|
<Project Path="LanMountainDesktop/LanMountainDesktop.csproj" />
|
||||||
|
</Solution>
|
||||||
@@ -271,6 +271,10 @@ public partial class App : Application
|
|||||||
mainWindow.Activate();
|
mainWindow.Activate();
|
||||||
mainWindow.Topmost = true;
|
mainWindow.Topmost = true;
|
||||||
mainWindow.Topmost = false;
|
mainWindow.Topmost = false;
|
||||||
|
if (mainWindow is MainWindow lanMountainMainWindow)
|
||||||
|
{
|
||||||
|
lanMountainMainWindow.ShowSingleInstanceNotice();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -743,7 +743,10 @@
|
|||||||
"placement.fit": "Fit",
|
"placement.fit": "Fit",
|
||||||
"placement.stretch": "Stretch",
|
"placement.stretch": "Stretch",
|
||||||
"placement.center": "Center",
|
"placement.center": "Center",
|
||||||
"placement.tile": "Tile"
|
"placement.tile": "Tile",
|
||||||
}
|
"single_instance.notice.title": "App already open",
|
||||||
|
"single_instance.notice.description": "LanMountainDesktop is already running. Switched back to the active desktop.",
|
||||||
|
"single_instance.notice.button": "Got it"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -743,7 +743,10 @@
|
|||||||
"placement.fit": "适应",
|
"placement.fit": "适应",
|
||||||
"placement.stretch": "拉伸",
|
"placement.stretch": "拉伸",
|
||||||
"placement.center": "居中",
|
"placement.center": "居中",
|
||||||
"placement.tile": "平铺"
|
"placement.tile": "平铺",
|
||||||
}
|
"single_instance.notice.title": "应用已打开",
|
||||||
|
"single_instance.notice.description": "阑山桌面已经在运行,已为你切换到当前正在使用的桌面。",
|
||||||
|
"single_instance.notice.button": "知道了"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.WebView.Desktop;
|
using Avalonia.WebView.Desktop;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop;
|
namespace LanMountainDesktop;
|
||||||
|
|
||||||
@@ -17,12 +19,11 @@ sealed class Program
|
|||||||
AppLogger.Initialize();
|
AppLogger.Initialize();
|
||||||
RegisterGlobalExceptionLogging();
|
RegisterGlobalExceptionLogging();
|
||||||
|
|
||||||
using var singleInstance = SingleInstanceService.CreateDefault();
|
using var singleInstance = AcquireSingleInstance(args);
|
||||||
if (!singleInstance.IsPrimaryInstance)
|
if (!singleInstance.IsPrimaryInstance)
|
||||||
{
|
{
|
||||||
AppLogger.Warn("Startup", "A secondary launch was blocked because another instance is already running.");
|
AppLogger.Warn("Startup", "A secondary launch was blocked because another instance is already running.");
|
||||||
var notified = singleInstance.TryNotifyPrimaryInstance(TimeSpan.FromSeconds(2));
|
_ = singleInstance.TryNotifyPrimaryInstance(TimeSpan.FromSeconds(2));
|
||||||
ShowAlreadyRunningNotice(notified);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +73,42 @@ sealed class Program
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static SingleInstanceService AcquireSingleInstance(string[] args)
|
||||||
|
{
|
||||||
|
var restartParentProcessId = AppRestartService.TryGetRestartParentProcessId(args);
|
||||||
|
var singleInstance = SingleInstanceService.CreateDefault();
|
||||||
|
if (singleInstance.IsPrimaryInstance || restartParentProcessId is null)
|
||||||
|
{
|
||||||
|
return singleInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLogger.Info(
|
||||||
|
"Startup",
|
||||||
|
$"Restart relaunch detected. Waiting for previous instance pid={restartParentProcessId.Value} to exit before re-acquiring the single-instance lock.");
|
||||||
|
singleInstance.Dispose();
|
||||||
|
|
||||||
|
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(12);
|
||||||
|
WaitForRestartParentExit(restartParentProcessId.Value, deadline);
|
||||||
|
|
||||||
|
while (DateTime.UtcNow < deadline)
|
||||||
|
{
|
||||||
|
var retryInstance = SingleInstanceService.CreateDefault();
|
||||||
|
if (retryInstance.IsPrimaryInstance)
|
||||||
|
{
|
||||||
|
AppLogger.Info("Startup", "Restart relaunch acquired the single-instance lock.");
|
||||||
|
return retryInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
retryInstance.Dispose();
|
||||||
|
Thread.Sleep(150);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLogger.Warn(
|
||||||
|
"Startup",
|
||||||
|
$"Restart relaunch timed out while waiting for the single-instance lock. pid={restartParentProcessId.Value}.");
|
||||||
|
return SingleInstanceService.CreateDefault();
|
||||||
|
}
|
||||||
|
|
||||||
private static string LoadConfiguredRenderMode()
|
private static string LoadConfiguredRenderMode()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -85,14 +122,25 @@ sealed class Program
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ShowAlreadyRunningNotice(bool notifiedPrimaryInstance)
|
private static void WaitForRestartParentExit(int processId, DateTime deadlineUtc)
|
||||||
{
|
{
|
||||||
const string caption = "LanMountainDesktop";
|
try
|
||||||
var message = notifiedPrimaryInstance
|
{
|
||||||
? "应用已打开,不需要多开了。\r\n\r\n已为你切换到正在运行的阑山桌面。"
|
using var process = Process.GetProcessById(processId);
|
||||||
: "应用已打开,不需要多开了。\r\n\r\n请切换到正在运行的阑山桌面。";
|
var remaining = deadlineUtc - DateTime.UtcNow;
|
||||||
|
if (remaining > TimeSpan.Zero)
|
||||||
WindowsNativeDialogService.ShowInformation(caption, message);
|
{
|
||||||
|
process.WaitForExit((int)Math.Ceiling(remaining.TotalMilliseconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
// The previous process already exited before we started waiting.
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn("Startup", $"Failed while waiting for restart parent pid={processId} to exit.", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterGlobalExceptionLogging()
|
private static void RegisterGlobalExceptionLogging()
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ namespace LanMountainDesktop.Services;
|
|||||||
|
|
||||||
public static class AppRestartService
|
public static class AppRestartService
|
||||||
{
|
{
|
||||||
|
private const string RestartParentPidArgumentPrefix = "--restart-parent-pid=";
|
||||||
|
|
||||||
public static bool TryRestartApplication()
|
public static bool TryRestartApplication()
|
||||||
{
|
{
|
||||||
return App.CurrentHostApplicationLifecycle?.TryRestart(new HostApplicationLifecycleRequest(
|
return App.CurrentHostApplicationLifecycle?.TryRestart(new HostApplicationLifecycleRequest(
|
||||||
@@ -75,6 +77,21 @@ public static class AppRestartService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int? TryGetRestartParentProcessId(IReadOnlyList<string> commandLineArgs)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(commandLineArgs);
|
||||||
|
|
||||||
|
foreach (var argument in commandLineArgs)
|
||||||
|
{
|
||||||
|
if (TryParseRestartParentProcessId(argument, out var processId))
|
||||||
|
{
|
||||||
|
return processId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static ProcessStartInfo CreateExecutableStartInfo(
|
private static ProcessStartInfo CreateExecutableStartInfo(
|
||||||
string executablePath,
|
string executablePath,
|
||||||
string? entryAssemblyPath,
|
string? entryAssemblyPath,
|
||||||
@@ -88,6 +105,7 @@ public static class AppRestartService
|
|||||||
};
|
};
|
||||||
|
|
||||||
AppendArguments(startInfo, commandLineArgs);
|
AppendArguments(startInfo, commandLineArgs);
|
||||||
|
AppendRestartParentProcessArgument(startInfo);
|
||||||
return startInfo;
|
return startInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +128,7 @@ public static class AppRestartService
|
|||||||
|
|
||||||
startInfo.ArgumentList.Add(entryAssemblyPath);
|
startInfo.ArgumentList.Add(entryAssemblyPath);
|
||||||
AppendArguments(startInfo, commandLineArgs);
|
AppendArguments(startInfo, commandLineArgs);
|
||||||
|
AppendRestartParentProcessArgument(startInfo);
|
||||||
return startInfo;
|
return startInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,10 +136,34 @@ public static class AppRestartService
|
|||||||
{
|
{
|
||||||
for (var i = 1; i < commandLineArgs.Count; i++)
|
for (var i = 1; i < commandLineArgs.Count; i++)
|
||||||
{
|
{
|
||||||
|
if (TryParseRestartParentProcessId(commandLineArgs[i], out _))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
startInfo.ArgumentList.Add(commandLineArgs[i]);
|
startInfo.ArgumentList.Add(commandLineArgs[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AppendRestartParentProcessArgument(ProcessStartInfo startInfo)
|
||||||
|
{
|
||||||
|
startInfo.ArgumentList.Add($"{RestartParentPidArgumentPrefix}{Environment.ProcessId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseRestartParentProcessId(string? argument, out int processId)
|
||||||
|
{
|
||||||
|
processId = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(argument) ||
|
||||||
|
!argument.StartsWith(RestartParentPidArgumentPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return int.TryParse(
|
||||||
|
argument[RestartParentPidArgumentPrefix.Length..],
|
||||||
|
out processId) && processId > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static string? NormalizeExistingPath(string? path)
|
private static string? NormalizeExistingPath(string? path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
|||||||
59
LanMountainDesktop/Views/MainWindow.SingleInstanceNotice.cs
Normal file
59
LanMountainDesktop/Views/MainWindow.SingleInstanceNotice.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow
|
||||||
|
{
|
||||||
|
private readonly DispatcherTimer _singleInstanceNoticeTimer = new()
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(6)
|
||||||
|
};
|
||||||
|
|
||||||
|
internal void ShowSingleInstanceNotice()
|
||||||
|
{
|
||||||
|
if (Dispatcher.UIThread.CheckAccess())
|
||||||
|
{
|
||||||
|
ShowSingleInstanceNoticeCore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(ShowSingleInstanceNoticeCore, DispatcherPriority.Send);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowSingleInstanceNoticeCore()
|
||||||
|
{
|
||||||
|
SingleInstanceNoticeTitleTextBlock.Text = L(
|
||||||
|
"single_instance.notice.title",
|
||||||
|
"App already open");
|
||||||
|
SingleInstanceNoticeDescriptionTextBlock.Text = L(
|
||||||
|
"single_instance.notice.description",
|
||||||
|
"LanMountainDesktop is already running. Switched back to the active desktop.");
|
||||||
|
SingleInstanceNoticeButtonTextBlock.Text = L(
|
||||||
|
"single_instance.notice.button",
|
||||||
|
"Got it");
|
||||||
|
SingleInstanceNoticeDock.IsVisible = true;
|
||||||
|
|
||||||
|
_singleInstanceNoticeTimer.Stop();
|
||||||
|
_singleInstanceNoticeTimer.Tick -= OnSingleInstanceNoticeTimerTick;
|
||||||
|
_singleInstanceNoticeTimer.Tick += OnSingleInstanceNoticeTimerTick;
|
||||||
|
_singleInstanceNoticeTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSingleInstanceNoticeButtonClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
HideSingleInstanceNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSingleInstanceNoticeTimerTick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
HideSingleInstanceNotice();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideSingleInstanceNotice()
|
||||||
|
{
|
||||||
|
_singleInstanceNoticeTimer.Stop();
|
||||||
|
SingleInstanceNoticeDock.IsVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -469,8 +469,54 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ui:NavigationView>
|
</ui:NavigationView>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1"
|
||||||
|
Spacing="12">
|
||||||
|
<Border x:Name="SingleInstanceNoticeDock"
|
||||||
|
IsVisible="False"
|
||||||
|
Classes="glass-panel"
|
||||||
|
CornerRadius="18"
|
||||||
|
Padding="14,12">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||||
|
ColumnSpacing="12">
|
||||||
|
<Border Width="34"
|
||||||
|
Height="34"
|
||||||
|
CornerRadius="17"
|
||||||
|
Background="{DynamicResource AdaptiveAccentBrush}">
|
||||||
|
<fi:FluentIcon Icon="Alert"
|
||||||
|
IconVariant="Regular"
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="White"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
<StackPanel Grid.Column="1"
|
||||||
|
Spacing="2"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<TextBlock x:Name="SingleInstanceNoticeTitleTextBlock"
|
||||||
|
FontSize="13"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Text="App already open" />
|
||||||
|
<TextBlock x:Name="SingleInstanceNoticeDescriptionTextBlock"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
|
||||||
|
Text="LanMountainDesktop is already running. Switched back to the active desktop." />
|
||||||
|
</StackPanel>
|
||||||
|
<Button x:Name="SingleInstanceNoticeButton"
|
||||||
|
Grid.Column="2"
|
||||||
|
Padding="14,8"
|
||||||
|
Click="OnSingleInstanceNoticeButtonClick">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<fi:FluentIcon Icon="Checkmark"
|
||||||
|
IconVariant="Regular" />
|
||||||
|
<TextBlock x:Name="SingleInstanceNoticeButtonTextBlock"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="Got it" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<Border x:Name="PendingRestartDock"
|
<Border x:Name="PendingRestartDock"
|
||||||
Grid.Row="1"
|
|
||||||
IsVisible="False"
|
IsVisible="False"
|
||||||
Classes="glass-panel"
|
Classes="glass-panel"
|
||||||
CornerRadius="18"
|
CornerRadius="18"
|
||||||
@@ -514,6 +560,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -78,9 +78,13 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await using var hashStream = File.OpenRead(downloadPath);
|
string actualHash;
|
||||||
|
await using (var hashStream = File.OpenRead(downloadPath))
|
||||||
|
{
|
||||||
var hashBytes = await SHA256.HashDataAsync(hashStream, cancellationToken);
|
var hashBytes = await SHA256.HashDataAsync(hashStream, cancellationToken);
|
||||||
var actualHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
|
actualHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.Equals(actualHash, plugin.Sha256, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(actualHash, plugin.Sha256, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
File.Delete(downloadPath);
|
File.Delete(downloadPath);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
- Windows 是当前主要目标平台。
|
- Windows 是当前主要目标平台。
|
||||||
- 已提供组件系统、插件系统、主题系统和设置系统。
|
- 已提供组件系统、插件系统、主题系统和设置系统。
|
||||||
- 中文为主语言,英文为附加扩展语言。
|
- 中文为主语言,英文为附加扩展语言。
|
||||||
|
- 仓库主入口解决方案文件已切换为 `LanMountainDesktop.slnx`,SDK 版本由根目录 `global.json` 锁定。
|
||||||
|
|
||||||
### 运行说明
|
### 运行说明
|
||||||
|
|
||||||
|
|||||||
6
global.json
Normal file
6
global.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "10.0.103",
|
||||||
|
"rollForward": "latestFeature"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
run.md
8
run.md
@@ -8,12 +8,14 @@
|
|||||||
|
|
||||||
- 安装 .NET SDK 10。
|
- 安装 .NET SDK 10。
|
||||||
- 桌面端建议在 Windows 上运行。
|
- 桌面端建议在 Windows 上运行。
|
||||||
|
- 仓库主入口解决方案文件为 `LanMountainDesktop.slnx`。
|
||||||
|
- SDK 版本由仓库根目录 `global.json` 锁定。
|
||||||
|
|
||||||
### 构建
|
### 构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet restore
|
dotnet restore
|
||||||
dotnet build LanMountainDesktop.sln -c Debug
|
dotnet build LanMountainDesktop.slnx -c Debug
|
||||||
```
|
```
|
||||||
|
|
||||||
### 运行桌面端
|
### 运行桌面端
|
||||||
@@ -41,11 +43,13 @@ dotnet run --project LanMountainDesktop/LanMountainDesktop.csproj
|
|||||||
|
|
||||||
This guide explains how to run LanMountainDesktop locally.
|
This guide explains how to run LanMountainDesktop locally.
|
||||||
|
|
||||||
|
The repository entry solution is `LanMountainDesktop.slnx`, and the SDK version is pinned by the root `global.json`.
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet restore
|
dotnet restore
|
||||||
dotnet build LanMountainDesktop.sln -c Debug
|
dotnet build LanMountainDesktop.slnx -c Debug
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run
|
### Run
|
||||||
|
|||||||
Reference in New Issue
Block a user