fix(installer): group WSL distributions under one context submenu
This commit is contained in:
1 parent
0110c7c139
commit
e054b09771
4 files changed
+223
-47
No files matched your search
@@ -0,0 +1,59 @@
|
||||
# Group WSL launch entries under one Explorer submenu
|
||||
|
||||
## Status
|
||||
|
||||
Implemented; pending review.
|
||||
|
||||
## Context
|
||||
|
||||
One top-level context item per WSL distribution crowds Explorer menus. The user
|
||||
requested one ordinary Pebrel entry and one expandable entry for distribution
|
||||
choices, for both selected directories and directory backgrounds.
|
||||
|
||||
## Evidence
|
||||
|
||||
The previous installer registered `PebrelWsl<index>` directly under each Explorer
|
||||
shell root. Those entries remain registered after an upgrade unless explicitly
|
||||
migrated. WSL distribution enumeration already excludes Docker plumbing entries.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep the ordinary entry. Register a static `PebrelWslMenu` cascade with an empty
|
||||
`SubCommands` value and its distribution verbs under `shell`. Record its executable
|
||||
owner and validate child commands before replacing or removing the subtree.
|
||||
Migrate only flat entries whose command invokes the exact owned executable.
|
||||
|
||||
Preserve `%1` for selected directories, `%V` for backgrounds, and the existing
|
||||
`--shell` before `--working-directory` order. No distributions means no empty
|
||||
submenu. Conflicting/edited subtrees and other installations remain untouched.
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
- Removing distribution choices reduces existing launch functionality.
|
||||
- Deleting every key with the product prefix can remove another installation or
|
||||
user customization.
|
||||
- Installing a new Explorer extension adds deployment and lifetime requirements
|
||||
unnecessary for this static cascade.
|
||||
|
||||
## Consequences
|
||||
|
||||
Registration still reflects distributions present at installation time. Existing
|
||||
menus migrate when the new installer runs; changing this source does not modify
|
||||
the user's live registry. Conflicts fail visibly rather than overwrite unknown keys.
|
||||
|
||||
## Validation
|
||||
|
||||
The real Inno fixture uses an isolated HKCU test subtree and passes 117 migration
|
||||
checks in total. New coverage includes both roots, zero/one/three distributions,
|
||||
Unicode/space-containing names, repeat registration, stale-entry removal, edited
|
||||
subtrees and foreign ownership. The complete installer compiles without executing
|
||||
an installation. Explorer presentation/click acceptance remains separate.
|
||||
|
||||
## Supersedes
|
||||
|
||||
The installer's flat WSL verb layout; the ordinary Pebrel verb is retained.
|
||||
|
||||
## Revisit when
|
||||
|
||||
Distribution registration must update outside installation, or Explorer requires
|
||||
a different supported menu registration mechanism.
|
||||
@@ -277,13 +277,13 @@ end;
|
||||
|
||||
{ —— 按 WSL 发行版注册的右键项 ——
|
||||
|
||||
「在 Pebrel 中打开(Ubuntu)」这类菜单项按**安装那一刻**机器上的发行版动态生成:
|
||||
WSL 发行版子菜单按**安装那一刻**机器上的发行版动态生成:
|
||||
`Lxss` 里的 `DistributionName` 是唯一真相,枚举口径与 Rust 侧
|
||||
`shell_detect::find_wsl_distros` 一致(跳过 `docker-desktop*` 这类 plumbing 发行版)。
|
||||
|
||||
走 `[Code]` 而不是 `[Registry]` 的原因只有一个:键的条数取决于机器上有几个发行版。
|
||||
代价是这些键没有 `uninsdeletekey`,卸载由 `RemoveOwnedWslContextMenus` 按名字前缀
|
||||
加命令串里的可执行文件与 `--shell` 认领后删除。 }
|
||||
加精确命令与级联菜单归属标记认领后删除。 }
|
||||
|
||||
const
|
||||
LxssRoot = 'Software\Microsoft\Windows\CurrentVersion\Lxss';
|
||||
@@ -308,67 +308,117 @@ begin
|
||||
SetArrayLength(Result, Count);
|
||||
end;
|
||||
|
||||
{ 删掉本安装器写下的 WSL 右键项:键名以 `PebrelWsl` 开头、命令串里同时有我们的
|
||||
可执行文件和 `--shell`,两条都满足才算我们写的。发行版后来被移除也不会留下
|
||||
点不动的死项。 }
|
||||
procedure RemoveOwnedWslContextMenus;
|
||||
{ WSL 右键项按精确可执行文件前缀归属;级联菜单还校验所有子项,保留用户编辑。
|
||||
发行版移除后,下次安装重建菜单时不留下旧项。 }
|
||||
function IsOwnedWslCommand(Command, Executable: string): Boolean;
|
||||
var
|
||||
Roots: array[0..1] of string;
|
||||
Root, Key, Command: string;
|
||||
Names: TArrayOfString;
|
||||
Index, NameIndex: Integer;
|
||||
Prefix: string;
|
||||
begin
|
||||
Prefix := '"' + Executable + '" --gpui --shell "wsl:';
|
||||
Result := CompareText(Copy(Command, 1, Length(Prefix)), Prefix) = 0;
|
||||
end;
|
||||
|
||||
function IsOwnedWslMenu(Key, Executable: string): Boolean;
|
||||
var
|
||||
Owner, Command: string;
|
||||
Children, Subkeys: TArrayOfString;
|
||||
Index: Integer;
|
||||
begin
|
||||
Result := False;
|
||||
if not RegQueryStringValue(HKCU, Key, 'PebrelOwner', Owner) or
|
||||
(CompareText(Owner, Executable) <> 0) then
|
||||
Exit;
|
||||
if not RegGetSubkeyNames(HKCU, Key, Subkeys) then
|
||||
Exit;
|
||||
for Index := 0 to GetArrayLength(Subkeys) - 1 do
|
||||
if CompareText(Subkeys[Index], 'shell') <> 0 then
|
||||
Exit;
|
||||
if GetArrayLength(Subkeys) > 0 then begin
|
||||
if not RegGetSubkeyNames(HKCU, Key + '\shell', Children) then
|
||||
Exit;
|
||||
for Index := 0 to GetArrayLength(Children) - 1 do begin
|
||||
if not RegQueryStringValue(HKCU, Key + '\shell\' + Children[Index] + '\command', '', Command) or
|
||||
not IsOwnedWslCommand(Command, Executable) then
|
||||
Exit;
|
||||
end;
|
||||
end;
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure RemoveOwnedWslContextMenusAt(Root, Executable: string);
|
||||
var
|
||||
Key, Command: string;
|
||||
Names: TArrayOfString;
|
||||
NameIndex: Integer;
|
||||
begin
|
||||
Roots[0] := 'Software\Classes\Directory\shell';
|
||||
Roots[1] := 'Software\Classes\Directory\Background\shell';
|
||||
for Index := 0 to 1 do begin
|
||||
Root := Roots[Index];
|
||||
if RegGetSubkeyNames(HKCU, Root, Names) then
|
||||
for NameIndex := 0 to GetArrayLength(Names) - 1 do
|
||||
if Pos('PebrelWsl', Names[NameIndex]) = 1 then begin
|
||||
Key := Root + '\' + Names[NameIndex];
|
||||
Command := '';
|
||||
if RegQueryStringValue(HKCU, Key + '\command', '', Command) and
|
||||
(Pos(ExpandConstant('{app}\pebrel.exe'), Command) > 0) and
|
||||
(Pos('--shell', Command) > 0) then
|
||||
if IsOwnedWslMenu(Key, Executable) or
|
||||
(RegQueryStringValue(HKCU, Key + '\command', '', Command) and
|
||||
IsOwnedWslCommand(Command, Executable)) then
|
||||
if not RegDeleteKeyIncludingSubkeys(HKCU, Key) then
|
||||
RaiseException('Unable to remove an owned WSL context menu: ' + Key);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure RemoveOwnedWslContextMenus;
|
||||
var
|
||||
Roots: array[0..1] of string;
|
||||
Index: Integer;
|
||||
begin
|
||||
Roots[0] := 'Software\Classes\Directory\shell';
|
||||
Roots[1] := 'Software\Classes\Directory\Background\shell';
|
||||
for Index := 0 to 1 do
|
||||
RemoveOwnedWslContextMenusAt(Roots[Index], ExpandConstant('{app}\pebrel.exe'));
|
||||
end;
|
||||
|
||||
procedure RegisterWslContextMenuAt(Root, Executable, DirectoryArgument: string;
|
||||
Distros: TArrayOfString);
|
||||
var
|
||||
Index: Integer;
|
||||
Distro, Verb, Command, Key, Menu: string;
|
||||
begin
|
||||
Menu := Root + '\PebrelWslMenu';
|
||||
{ Never overwrite an unknown installation or an edited submenu. }
|
||||
if RegKeyExists(HKCU, Menu) and not IsOwnedWslMenu(Menu, Executable) then
|
||||
RaiseException('An unowned or edited WSL context menu was preserved: ' + Menu);
|
||||
RemoveOwnedWslContextMenusAt(Root, Executable);
|
||||
if GetArrayLength(Distros) = 0 then
|
||||
Exit;
|
||||
if not RegWriteStringValue(HKCU, Menu, 'MUIVerb', CustomMessage('OpenInPebrelWsl')) or
|
||||
not RegWriteStringValue(HKCU, Menu, 'Icon', Executable + ',0') or
|
||||
not RegWriteStringValue(HKCU, Menu, 'SubCommands', '') or
|
||||
not RegWriteStringValue(HKCU, Menu, 'PebrelOwner', Executable) then
|
||||
RaiseException('Unable to register the WSL context submenu.');
|
||||
for Index := 0 to GetArrayLength(Distros) - 1 do begin
|
||||
Distro := Distros[Index];
|
||||
{ 键名用序号而不是发行版名:注册表键名里带空格与非 ASCII 只会给自己找麻烦,
|
||||
何况我们靠前缀认领。 }
|
||||
Verb := 'PebrelWsl' + IntToStr(Index);
|
||||
{ `--shell` 必须排在 `--working-directory` 之前:盘根(`D:\`)时后者的收尾
|
||||
反斜杠会吃掉它的收尾引号,并把后面整段并进同一个参数(issue #36 的另一面),
|
||||
顺序写反会静默开出一个既没有 cwd、也没用上指定发行版的标签。 }
|
||||
Command := '"' + Executable + '" --gpui --shell "wsl:' + Distro + '" --working-directory ';
|
||||
Key := Menu + '\shell\' + Verb;
|
||||
if not RegWriteStringValue(HKCU, Key, 'MUIVerb', Distro) or
|
||||
not RegWriteStringValue(HKCU, Key, 'Icon', Executable + ',0') or
|
||||
not RegWriteStringValue(HKCU, Key + '\command', '', Command + '"' + DirectoryArgument + '"') then
|
||||
RaiseException('Unable to register the WSL context menu for ' + Distro + '.');
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure RegisterWslContextMenus;
|
||||
var
|
||||
Distros: TArrayOfString;
|
||||
Index: Integer;
|
||||
Distro, Verb, LabelText, Executable, Command, Key: string;
|
||||
Executable: string;
|
||||
begin
|
||||
{ 先清掉上一次写下的:发行版被移除后,残留的索引键会指向一个已经不存在的发行版。 }
|
||||
RemoveOwnedWslContextMenus;
|
||||
Distros := WslDistroNames;
|
||||
if GetArrayLength(Distros) = 0 then
|
||||
Exit;
|
||||
Executable := ExpandConstant('{app}\pebrel.exe');
|
||||
for Index := 0 to GetArrayLength(Distros) - 1 do begin
|
||||
Distro := Distros[Index];
|
||||
{ 键名用序号而不是发行版名:注册表键名里带空格与非 ASCII 只会给自己找麻烦,
|
||||
何况我们靠前缀认领。 }
|
||||
Verb := 'PebrelWsl' + IntToStr(Index);
|
||||
LabelText := CustomMessage('OpenInPebrelWsl') + ' (' + Distro + ')';
|
||||
{ `--shell` 必须排在 `--working-directory` 之前:盘根(`D:\`)时后者的收尾
|
||||
反斜杠会吃掉它的收尾引号,并把后面整段并进同一个参数(issue #36 的另一面),
|
||||
顺序写反会静默开出一个既没有 cwd、也没用上指定发行版的标签。 }
|
||||
Command := '"' + Executable + '" --gpui --shell "wsl:' + Distro + '" --working-directory ';
|
||||
Key := 'Software\Classes\Directory\shell\' + Verb;
|
||||
if not RegWriteStringValue(HKCU, Key, 'MUIVerb', LabelText) or
|
||||
not RegWriteStringValue(HKCU, Key, 'Icon', Executable + ',0') or
|
||||
not RegWriteStringValue(HKCU, Key + '\command', '', Command + '"%1"') then
|
||||
RaiseException('Unable to register the WSL context menu for ' + Distro + '.');
|
||||
Key := 'Software\Classes\Directory\Background\shell\' + Verb;
|
||||
if not RegWriteStringValue(HKCU, Key, 'MUIVerb', LabelText) or
|
||||
not RegWriteStringValue(HKCU, Key, 'Icon', Executable + ',0') or
|
||||
not RegWriteStringValue(HKCU, Key + '\command', '', Command + '"%V"') then
|
||||
RaiseException('Unable to register the WSL background context menu for ' + Distro + '.');
|
||||
end;
|
||||
RegisterWslContextMenuAt('Software\Classes\Directory\shell', Executable, '%1', Distros);
|
||||
RegisterWslContextMenuAt('Software\Classes\Directory\Background\shell', Executable, '%V', Distros);
|
||||
end;
|
||||
|
||||
procedure MigrateLegacyIntegrations;
|
||||
|
||||
@@ -77,7 +77,7 @@ english.AutoStart=Start Pebrel when I sign in to Windows
|
||||
english.InstallFont=Install Maple Mono font for the current user
|
||||
english.AddToPath=Add Pebrel to the user PATH
|
||||
english.OpenInPebrel=Open in Pebrel
|
||||
english.OpenInPebrelWsl=Open in Pebrel
|
||||
english.OpenInPebrelWsl=Open in Pebrel (WSL)
|
||||
english.LaunchProgram=Launch Pebrel
|
||||
english.UninstallProgram=Uninstall Pebrel
|
||||
english.CloseLegacyProgram=Close the application at %1, then retry the installation.
|
||||
@@ -89,7 +89,7 @@ chinesesimplified.AutoStart=登录 Windows 后启动 Pebrel
|
||||
chinesesimplified.InstallFont=为当前用户安装 Maple Mono 字体
|
||||
chinesesimplified.AddToPath=将 Pebrel 添加到当前用户 PATH
|
||||
chinesesimplified.OpenInPebrel=在 Pebrel 中打开
|
||||
chinesesimplified.OpenInPebrelWsl=在 Pebrel 中打开
|
||||
chinesesimplified.OpenInPebrelWsl=在 Pebrel 中打开(WSL)
|
||||
chinesesimplified.LaunchProgram=启动 Pebrel
|
||||
chinesesimplified.UninstallProgram=卸载 Pebrel
|
||||
chinesesimplified.CloseLegacyProgram=请关闭 %1 中运行的程序,然后重试安装。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#ifndef FixtureRoot
|
||||
#ifndef FixtureRoot
|
||||
#error FixtureRoot must point at an isolated directory under the workspace tmp directory.
|
||||
#endif
|
||||
#define MigrationFixture
|
||||
@@ -15,6 +15,9 @@ OutputBaseFilename=installer-migration-fixture
|
||||
Compression=none
|
||||
SetupLogging=no
|
||||
|
||||
[CustomMessages]
|
||||
OpenInPebrelWsl=Open in Pebrel (WSL)
|
||||
|
||||
[Code]
|
||||
#include "..\installer-migration.iss"
|
||||
|
||||
@@ -180,6 +183,69 @@ begin
|
||||
Check(FileExists(NewLink), 'shortcut pointing at another program is preserved');
|
||||
end;
|
||||
|
||||
procedure CheckContextMenus(Root: string);
|
||||
var
|
||||
RegistryRoot, ShellRoot, Executable, Value, Menu, ForeignCommand: string;
|
||||
Distros, Names: TArrayOfString;
|
||||
Index: Integer;
|
||||
Failed: Boolean;
|
||||
begin
|
||||
RegistryRoot := 'Software\PebrelTestFixtures\' + ExtractFileName(Root);
|
||||
Executable := Root + '\Program Files\pebrel.exe';
|
||||
ForeignCommand := '"D:\other\pebrel.exe" --gpui --shell "wsl:Other" --working-directory "%1"';
|
||||
try
|
||||
for Index := 0 to 1 do begin
|
||||
ShellRoot := RegistryRoot + '\root' + IntToStr(Index);
|
||||
Menu := ShellRoot + '\PebrelWslMenu';
|
||||
Check(RegWriteStringValue(HKCU, ShellRoot + '\Pebrel\command', '', 'ordinary-open'), 'seed ordinary entry');
|
||||
Check(RegWriteStringValue(HKCU, ShellRoot + '\PebrelWslForeign\command', '', ForeignCommand), 'seed foreign entry');
|
||||
Check(RegWriteStringValue(HKCU, ShellRoot + '\PebrelWsl0\command', '',
|
||||
'"' + Executable + '" --gpui --shell "wsl:Old" --working-directory "%1"'), 'seed old flat entry');
|
||||
SetArrayLength(Distros, 3);
|
||||
Distros[0] := 'Debian';
|
||||
Distros[1] := 'Ubuntu Test';
|
||||
Distros[2] := '开发环境';
|
||||
if Index = 0 then Value := '%1' else Value := '%V';
|
||||
RegisterWslContextMenuAt(ShellRoot, Executable, Value, Distros);
|
||||
Check(not RegKeyExists(HKCU, ShellRoot + '\PebrelWsl0'), 'migrate old flat entry');
|
||||
Check(RegGetSubkeyNames(HKCU, ShellRoot, Names) and (GetArrayLength(Names) = 3), 'only normal, cascade and foreign roots remain');
|
||||
Check(RegGetSubkeyNames(HKCU, Menu + '\shell', Names) and (GetArrayLength(Names) = 3), 'three distros live inside cascade');
|
||||
Check(RegQueryStringValue(HKCU, Menu, 'SubCommands', Value) and (Value = ''), 'enable static cascade');
|
||||
Check(RegQueryStringValue(HKCU, Menu + '\shell\PebrelWsl1\command', '', Value), 'child command exists');
|
||||
if Index = 0 then
|
||||
Check(Value = '"' + Executable + '" --gpui --shell "wsl:Ubuntu Test" --working-directory "%1"', 'selected directory argv')
|
||||
else
|
||||
Check(Value = '"' + Executable + '" --gpui --shell "wsl:Ubuntu Test" --working-directory "%V"', 'background directory argv');
|
||||
Check(RegQueryStringValue(HKCU, Menu + '\shell\PebrelWsl2', 'MUIVerb', Value) and (Value = '开发环境'), 'Unicode label retained');
|
||||
RegisterWslContextMenuAt(ShellRoot, Executable, '%1', Distros);
|
||||
Check(RegGetSubkeyNames(HKCU, Menu + '\shell', Names) and (GetArrayLength(Names) = 3), 'repeat registration has no duplicates');
|
||||
SetArrayLength(Distros, 1);
|
||||
RegisterWslContextMenuAt(ShellRoot, Executable, '%1', Distros);
|
||||
Check(RegGetSubkeyNames(HKCU, Menu + '\shell', Names) and (GetArrayLength(Names) = 1), 'removed distros leave no stale entries');
|
||||
Check(RegWriteStringValue(HKCU, Menu + '\shell\Custom\command', '', ForeignCommand), 'seed edited submenu');
|
||||
Failed := False;
|
||||
try
|
||||
RegisterWslContextMenuAt(ShellRoot, Executable, '%1', Distros);
|
||||
except
|
||||
Failed := True;
|
||||
end;
|
||||
Check(Failed and RegKeyExists(HKCU, Menu + '\shell\Custom'), 'edited submenu is not overwritten');
|
||||
RemoveOwnedWslContextMenusAt(ShellRoot, Executable);
|
||||
Check(RegKeyExists(HKCU, Menu + '\shell\Custom'), 'uninstall preserves edited subtree');
|
||||
Check(RegDeleteKeyIncludingSubkeys(HKCU, Menu + '\shell\Custom'), 'remove fixture edit');
|
||||
RemoveOwnedWslContextMenusAt(ShellRoot, 'D:\other\install.exe');
|
||||
Check(RegKeyExists(HKCU, Menu), 'other installation cannot remove this menu');
|
||||
SetArrayLength(Distros, 0);
|
||||
RegisterWslContextMenuAt(ShellRoot, Executable, '%1', Distros);
|
||||
Check(not RegKeyExists(HKCU, Menu), 'zero distros removes empty cascade');
|
||||
Check(RegQueryStringValue(HKCU, ShellRoot + '\PebrelWslForeign\command', '', Value) and (Value = ForeignCommand), 'foreign entry preserved');
|
||||
Check(RegQueryStringValue(HKCU, ShellRoot + '\Pebrel\command', '', Value) and (Value = 'ordinary-open'), 'ordinary entry preserved');
|
||||
end;
|
||||
finally
|
||||
RegDeleteKeyIncludingSubkeys(HKCU, RegistryRoot);
|
||||
end;
|
||||
end;
|
||||
|
||||
function InitializeSetup: Boolean;
|
||||
var
|
||||
Root, Report: string;
|
||||
@@ -190,6 +256,7 @@ begin
|
||||
CheckPayloadMigration(Root);
|
||||
CheckFailures(Root);
|
||||
CheckShortcuts(Root);
|
||||
CheckContextMenus(Root);
|
||||
Report := 'PASS: ' + IntToStr(Checks) + ' migration checks';
|
||||
except
|
||||
Report := 'FAIL: ' + GetExceptionMessage;
|
||||
|
||||
Reference in new issue
Block a user