返回提交历史
Modified
XFEToolBox.Client.Wpf.Test/ToolRuntimeProcessStartInfoFactoryTests.cs
+27
-0
Modified
XFEToolBox.Client.Wpf.Test/XFEToolBox.Client.Wpf.Test.csproj
+1
-0
Modified
XFEToolBox.Core/Tools/ToolCatalogContracts.cs
+2
-0
Modified
XFEToolBox.Core/Tools/ToolPackageManifest.cs
+6
-0
Modified
XFEToolBox.Server.Test/Program.cs
+2
-0
Modified
XFEToolBox.Server/Services/ToolPackageContractMapper.cs
+1
-0
Modified
XFEToolBox/Utilities/ToolLaunchPreferenceService.cs
+6
-37
Added
XFEToolBox/Utilities/ToolLaunchPreferenceStore.cs
+45
-0
Modified
XFEToolBox/Utilities/ToolProjectRunService.cs
+22
-3
Modified
XFEToolBox/Utilities/ToolProjectWorkspaceService.cs
+1
-0
Modified
XFEToolBox/Utilities/ToolRuntimeProcessStartInfoFactory.cs
+3
-0
Modified
XFEToolBox/ViewModel/Pages/ToolCardViewModel.cs
+17
-4
Modified
XFEToolBox/Views/Pages/Popups/ToolConfigurationPopupPage.xaml
+4
-1
Modified
XFEToolBox/Views/Pages/Popups/ToolConfigurationPopupPage.xaml.cs
+17
-4
Modified
XFEToolBox/Views/Pages/ToolBoxPage.xaml
+1
-1
Modified
XFEToolBox/Views/Pages/ToolBoxPage.xaml.cs
+12
-8
Modified
XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml
+16
-1
Modified
XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml.cs
+2
-0
Modified
XFEToolBox/XFEToolBox.Client.csproj
+1
-1
Modified
docs/tool-packages.md
+4
-0
XFEstudio/XFEToolBox
支持工具强制管理员模式,提升至 1.1.5
新增 requiresAdministrator 字段,支持在 manifest.json 声明强制管理员启动。ToolPackageManifest、ToolPackageSummary、工具包模板等同步更新。ToolCardViewModel 区分用户配置与 manifest 强制,UI 提示优化,强制时用户无法关闭管理员模式。新增 ToolLaunchPreferenceStore,统一管理用户偏好,替换原 ToolLaunchPreferenceService。ToolRuntimeProcessStartInfoFactory 合并用户与 manifest 需求,ToolProjectRunService 启动流程适配 UAC 提权及令牌校验。工具配置弹窗、卡片、启动流程等 UI 逻辑适配,编辑器支持可视化勾选强制管理员。补充相关测试用例。
6b8b6db
代码差异
20 个文件
+190
-60
@@ -5,6 +5,33 @@ namespace XFEToolBox.Client.Wpf.Test;
5
5
6
6
public static class ToolRuntimeProcessStartInfoFactoryTests
7
7
{
8
[Test]
9
public static void UserAndManifestAdministratorModesAreBothEffective()
10
{
11
Ensure(!ToolRuntimeProcessStartInfoFactory.ResolveRunAsAdministrator(false, false),
12
"未配置管理员模式时不应提权。 ");
13
Ensure(ToolRuntimeProcessStartInfoFactory.ResolveRunAsAdministrator(true, false),
14
"用户勾选管理员模式后没有进入提权启动。 ");
15
Ensure(ToolRuntimeProcessStartInfoFactory.ResolveRunAsAdministrator(false, true),
16
"manifest 强制管理员模式后没有进入提权启动。 ");
17
Ensure(ToolRuntimeProcessStartInfoFactory.ResolveRunAsAdministrator(true, true),
18
"用户配置和 manifest 同时启用时丢失管理员模式。 ");
19
}
20
21
[Test]
22
public static void AdministratorPreferenceSurvivesSerializationAndIsCaseInsensitive()
23
{
24
var json = ToolLaunchPreferenceStore.SetRunAsAdministrator(null, "xfestudio.sample-tool", true);
25
Ensure(ToolLaunchPreferenceStore.GetRunAsAdministrator(json, "XFESTUDIO.SAMPLE-TOOL"),
26
"管理员启动配置写入后没有读回。 ");
27
28
json = ToolLaunchPreferenceStore.SetRunAsAdministrator(json, "xfestudio.sample-tool", false);
29
Ensure(!ToolLaunchPreferenceStore.GetRunAsAdministrator(json, "xfestudio.sample-tool"),
30
"关闭管理员模式后配置仍然生效。 ");
31
Ensure(!ToolLaunchPreferenceStore.GetRunAsAdministrator("{invalid", "xfestudio.sample-tool"),
32
"损坏的旧配置不应导致工具被意外提权。 ");
33
}
34
8
35
[Test]
9
36
public static void AdministratorLaunchUsesShellRunAsAndTheCompiledAppHost()
10
37
{
@@ -25,6 +25,7 @@
25
25
<Compile Include="..\XFEToolBox.Client.Installer\Utilities\ZipHelper.cs" Link="Installer\ZipHelper.cs" />
26
26
<Compile Include="..\XFEToolBox\Utilities\WebImageSourceLoader.cs" Link="Utilities\WebImageSourceLoader.cs" />
27
27
<Compile Include="..\XFEToolBox\Utilities\ToolRuntimeProcessStartInfoFactory.cs" Link="Utilities\ToolRuntimeProcessStartInfoFactory.cs" />
28
<Compile Include="..\XFEToolBox\Utilities\ToolLaunchPreferenceStore.cs" Link="Utilities\ToolLaunchPreferenceStore.cs" />
28
29
<Compile Include="..\XFEToolBox\Models\RecentUsageEntry.cs" Link="Models\RecentUsageEntry.cs" />
29
30
<Compile Include="..\XFEToolBox\Utilities\RecentUsageIconCache.cs" Link="Utilities\RecentUsageIconCache.cs" />
30
31
</ItemGroup>
@@ -18,6 +18,8 @@ public sealed class ToolPackageSummary
18
18
19
19
public required string[] Tags { get; init; }
20
20
21
public bool RequiresAdministrator { get; init; }
22
21
23
public DateTimeOffset UpdatedAtUtc { get; init; }
22
24
}
23
25
@@ -38,6 +38,12 @@ public sealed class ToolPackageManifest
38
38
39
39
public string? ReleaseNotes { get; init; }
40
40
41
/// <summary>
42
/// Requires the host to display elevation state and launch this tool through Windows UAC.
43
/// Users cannot override this requirement with a per-tool preference.
44
/// </summary>
45
public bool RequiresAdministrator { get; init; }
46
41
47
public required ToolEntryManifest Entry { get; init; }
42
48
43
49
/// <summary>
@@ -41,6 +41,7 @@ static void ValidPackagePasses()
41
41
using var package = CreatePackage();
42
42
var result = CreateValidator().Inspect(package);
43
43
Assert(result.Manifest.Id == "base64-generator", "工具 ID 不正确。");
44
Assert(result.Manifest.RequiresAdministrator, "管理员启动要求没有从 manifest 保留下来。");
44
45
Assert(result.Files.Contains("src/Views/Base64Tool.xaml"), "未发现入口 XAML。");
45
46
Assert(result.Files.Contains("src/ViewModels/Base64ToolViewModel.cs"), "未发现 ViewModel。");
46
47
Assert(result.IconDataUrl?.StartsWith("data:image/png;base64,", StringComparison.Ordinal) == true, "工具图标未被读取。");
@@ -206,6 +207,7 @@ static MemoryStream CreatePackage(Action<ZipArchive>? customize = null)
206
207
Category = "编码",
207
208
Tags = ["base64"],
208
209
MinimumHostVersion = "0.2.0",
210
RequiresAdministrator = true,
209
211
Entry = new ToolEntryManifest
210
212
{
211
213
ViewXaml = "src/Views/Base64Tool.xaml",
@@ -15,6 +15,7 @@ internal static class ToolPackageContractMapper
15
15
Category = package.Manifest.Category,
16
16
LatestVersion = package.Manifest.Version,
17
17
Tags = package.Manifest.Tags,
18
RequiresAdministrator = package.Manifest.RequiresAdministrator,
18
19
UpdatedAtUtc = package.UploadedAtUtc
19
20
};
20
21
@@ -1,48 +1,17 @@
1
using System.Text.Json;
2
1
using XFEToolBox.Client.Profiles.CacheProfiles;
3
2
4
3
namespace XFEToolBox.Client.Utilities;
5
4
6
5
internal static class ToolLaunchPreferenceService
7
6
{
8
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
9
10
public static bool GetRunAsAdministrator(string toolId)
11
{
12
if (string.IsNullOrWhiteSpace(toolId)) return false;
13
return ReadPreferences().TryGetValue(toolId, out var preference) && preference.RunAsAdministrator;
14
}
7
public static bool GetRunAsAdministrator(string toolId) =>
8
ToolLaunchPreferenceStore.GetRunAsAdministrator(AppCacheProfile.ToolLaunchPreferencesJson, toolId);
15
9
16
10
public static void SetRunAsAdministrator(string toolId, bool runAsAdministrator)
17
11
{
18
ArgumentException.ThrowIfNullOrWhiteSpace(toolId);
19
var preferences = ReadPreferences();
20
if (runAsAdministrator)
21
preferences[toolId] = new ToolLaunchPreference(true);
22
else
23
preferences.Remove(toolId);
24
25
AppCacheProfile.ToolLaunchPreferencesJson = JsonSerializer.Serialize(preferences, JsonOptions);
12
AppCacheProfile.ToolLaunchPreferencesJson = ToolLaunchPreferenceStore.SetRunAsAdministrator(
13
AppCacheProfile.ToolLaunchPreferencesJson,
14
toolId,
15
runAsAdministrator);
26
16
}
27
28
private static Dictionary<string, ToolLaunchPreference> ReadPreferences()
29
{
30
try
31
{
32
var json = AppCacheProfile.ToolLaunchPreferencesJson;
33
if (string.IsNullOrWhiteSpace(json))
34
return new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase);
35
36
var stored = JsonSerializer.Deserialize<Dictionary<string, ToolLaunchPreference>>(json, JsonOptions);
37
return stored is null
38
? new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase)
39
: new Dictionary<string, ToolLaunchPreference>(stored, StringComparer.OrdinalIgnoreCase);
40
}
41
catch (JsonException)
42
{
43
return new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase);
44
}
45
}
46
47
private sealed record ToolLaunchPreference(bool RunAsAdministrator);
48
17
}
@@ -0,0 +1,45 @@
1
using System.Text.Json;
2
3
namespace XFEToolBox.Client.Utilities;
4
5
internal static class ToolLaunchPreferenceStore
6
{
7
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
8
9
public static bool GetRunAsAdministrator(string? json, string toolId)
10
{
11
if (string.IsNullOrWhiteSpace(toolId)) return false;
12
return Read(json).TryGetValue(toolId, out var preference) && preference.RunAsAdministrator;
13
}
14
15
public static string SetRunAsAdministrator(string? json, string toolId, bool runAsAdministrator)
16
{
17
ArgumentException.ThrowIfNullOrWhiteSpace(toolId);
18
var preferences = Read(json);
19
if (runAsAdministrator)
20
preferences[toolId] = new ToolLaunchPreference(true);
21
else
22
preferences.Remove(toolId);
23
return JsonSerializer.Serialize(preferences, JsonOptions);
24
}
25
26
private static Dictionary<string, ToolLaunchPreference> Read(string? json)
27
{
28
try
29
{
30
if (string.IsNullOrWhiteSpace(json))
31
return new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase);
32
33
var stored = JsonSerializer.Deserialize<Dictionary<string, ToolLaunchPreference>>(json, JsonOptions);
34
return stored is null
35
? new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase)
36
: new Dictionary<string, ToolLaunchPreference>(stored, StringComparer.OrdinalIgnoreCase);
37
}
38
catch (JsonException)
39
{
40
return new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase);
41
}
42
}
43
44
private sealed record ToolLaunchPreference(bool RunAsAdministrator);
45
}
@@ -108,6 +108,10 @@ internal static class ToolProjectRunService
108
108
var assemblyName = $"XFEToolRuntime_{Guid.NewGuid():N}";
109
109
var hostAssemblyName = typeof(ToolProjectRunService).Assembly.GetName().Name
110
110
?? throw new InvalidOperationException("无法确定宿主程序集名称。");
111
var requiresElevatedProcess = launchAfterBuild &&
112
ToolRuntimeProcessStartInfoFactory.ResolveRunAsAdministrator(
113
runAsAdministrator,
114
manifest.RequiresAdministrator);
111
115
var preparedWorkspaceRoot = Path.Combine(runtimeRoot, "source");
112
116
await PrepareWorkspaceAsync(
113
117
workspaceRoot,
@@ -118,7 +122,11 @@ internal static class ToolProjectRunService
118
122
var entryPath = Path.Combine(runtimeRoot, "RuntimeEntry.g.cs");
119
123
var toolIconPath = ResolveToolIconPath(preparedWorkspaceRoot, manifest.Icon);
120
124
await File.WriteAllTextAsync(projectPath, CreateProjectFile(preparedWorkspaceRoot, assemblyName, hostAssemblyName), new UTF8Encoding(false), cancellationToken);
121
await File.WriteAllTextAsync(entryPath, CreateRuntimeEntry(manifest, hostAssemblyName, windowTitle, toolIconPath), new UTF8Encoding(false), cancellationToken);
125
await File.WriteAllTextAsync(
126
entryPath,
127
CreateRuntimeEntry(manifest, hostAssemblyName, windowTitle, toolIconPath, requiresElevatedProcess),
128
new UTF8Encoding(false),
129
cancellationToken);
122
130
123
131
var buildInfo = new ProcessStartInfo("dotnet")
124
132
{
@@ -169,7 +177,7 @@ internal static class ToolProjectRunService
169
177
var runInfo = ToolRuntimeProcessStartInfoFactory.Create(
170
178
runtimeExecutable,
171
179
workspaceRoot,
172
runAsAdministrator);
180
requiresElevatedProcess);
173
181
var runtimeProcess = Process.Start(runInfo)
174
182
?? throw new InvalidOperationException("工具运行进程启动失败。");
175
183
var runtimeStandardOutputTask = runInfo.RedirectStandardOutput
@@ -283,7 +291,8 @@ internal static class ToolProjectRunService
283
291
ToolPackageManifest manifest,
284
292
string hostAssemblyName,
285
293
string windowTitle,
286
string? toolIconPath)
294
string? toolIconPath,
295
bool requiresElevatedProcess)
287
296
{
288
297
var window = NormalizeWindowSettings(manifest.Window);
289
298
var toolId = JsonSerializer.Serialize(manifest.Id.Trim());
@@ -309,9 +318,11 @@ internal static class ToolProjectRunService
309
318
var allowMaximize = JsonSerializer.Serialize(window.AllowMaximize);
310
319
var showMinimizeButton = JsonSerializer.Serialize(window.ShowMinimizeButton);
311
320
var showCloseButton = JsonSerializer.Serialize(window.ShowCloseButton);
321
var requiresAdministrator = JsonSerializer.Serialize(requiresElevatedProcess);
312
322
return $$"""
313
323
using System.IO;
314
324
using System.Reflection;
325
using System.Security.Principal;
315
326
using System.Windows;
316
327
using System.Windows.Controls;
317
328
using System.Windows.Media;
@@ -340,6 +351,8 @@ internal static class ToolProjectRunService
340
351
});
341
352
try
342
353
{
354
if ({{requiresAdministrator}} && !IsCurrentProcessAdministrator())
355
throw new UnauthorizedAccessException("工具要求管理员权限,但运行进程没有获得管理员令牌。");
343
356
ToolDataStore.Initialize({{toolId}});
344
357
var viewType = Assembly.GetExecutingAssembly().GetType({{viewClass}}, throwOnError: true)!;
345
358
var instance = Activator.CreateInstance(viewType)
@@ -369,6 +382,12 @@ internal static class ToolProjectRunService
369
382
}
370
383
}
371
384
385
private static bool IsCurrentProcessAdministrator()
386
{
387
using var identity = WindowsIdentity.GetCurrent();
388
return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
389
}
390
372
391
private static void ConfigureWindow(Application application, Window window, object content)
373
392
{
374
393
var savedPlacement = ToolDataStore.ReadWindowPlacement();
@@ -67,6 +67,7 @@ internal static class ToolProjectWorkspaceService
67
67
"icon": "{{DefaultToolIconRelativePath}}",
68
68
"category": "开发工具",
69
69
"tags": [ "WPF" ],
70
"requiresAdministrator": false,
70
71
"entry": {
71
72
"viewXaml": "Code/Views/MainPage.xaml",
72
73
"viewClass": "{{toolNamespace}}.MainPage",
@@ -5,6 +5,9 @@ namespace XFEToolBox.Client.Utilities;
5
5
6
6
internal static class ToolRuntimeProcessStartInfoFactory
7
7
{
8
public static bool ResolveRunAsAdministrator(bool userPreference, bool manifestRequirement) =>
9
userPreference || manifestRequirement;
10
8
11
public static ProcessStartInfo Create(
9
12
string runtimeExecutable,
10
13
string workingDirectory,
@@ -17,7 +17,7 @@ public sealed class ToolCardViewModel(
17
17
private bool _isDownloadIndeterminate;
18
18
private double _downloadProgress;
19
19
private string _downloadProgressText = string.Empty;
20
private bool _runAsAdministrator = runAsAdministrator;
20
private bool _userRunAsAdministrator = runAsAdministrator;
21
21
22
22
public ToolPackageSummary Package { get; } = package;
23
23
public string Id => Package.Id;
@@ -29,12 +29,25 @@ public sealed class ToolCardViewModel(
29
29
public ImageSource IconSource { get; } = iconSource;
30
30
public ImageSource UacIconSource { get; } = uacIconSource;
31
31
32
public bool RunAsAdministrator
32
public bool RequiresAdministrator => Package.RequiresAdministrator;
33
34
public bool UserRunAsAdministrator
33
35
{
34
get => _runAsAdministrator;
35
set => SetProperty(ref _runAsAdministrator, value);
36
get => _userRunAsAdministrator;
37
set
38
{
39
if (!SetProperty(ref _userRunAsAdministrator, value)) return;
40
OnPropertyChanged(nameof(RunAsAdministrator));
41
OnPropertyChanged(nameof(AdministratorLaunchDescription));
42
}
36
43
}
37
44
45
public bool RunAsAdministrator => RequiresAdministrator || UserRunAsAdministrator;
46
47
public string AdministratorLaunchDescription => RequiresAdministrator
48
? "工具清单要求以管理员身份启动,用户无法关闭"
49
: "已由用户配置为以管理员身份打开";
50
38
51
public bool IsEnabled
39
52
{
40
53
get => _isEnabled;