仅在需要时使 Inno Setup 安装程序请求权限提升
Posted
技术标签:
【中文标题】仅在需要时使 Inno Setup 安装程序请求权限提升【英文标题】:Make Inno Setup installer request privileges elevation only when needed 【发布时间】:2014-02-28 16:19:18 【问题描述】:Inno Setup 安装程序具有PrivilegesRequired
directive,可用于控制安装程序启动时是否需要特权提升。我希望我的安装程序即使对于非管理员用户也能工作(将我的应用程序安装到用户文件夹而不是 Program Files
没有问题)。所以我将PrivilegesRequired
设置为none
(未记录的值)。这使得 UAC 提示仅针对管理员用户弹出,因此他们甚至可以安装到 Program Files
。非管理员用户没有 UAC 提示,因此即使他们也可以安装应用程序(到用户文件夹)。
这有一些缺点:
有些人在他们的机器上使用不同的管理员和非管理员帐户,正常使用非管理员帐户。通常,当使用非管理员帐户启动安装时,当他们收到 UAC 提示时,他们会输入管理员帐户的凭据以继续。但这不适用于我的安装程序,因为没有 UAC 提示。 (非常可疑)拥有管理员帐户的人想要安装到用户文件夹,如果没有(不需要)管理员权限,则无法启动我的安装程序。是否有某种方法可以仅在需要时(当用户选择仅可由管理员帐户写入的安装文件夹时)使 Inno Setup 请求权限提升?
我认为 Inno Setup 中没有此设置。但可能有一个编程解决方案(Inno Setup Pascal 脚本)或某种插件/DLL。
请注意,Inno Setup 6 内置了对 non-administrative install mode 的支持。
【问题讨论】:
【参考方案1】:Inno Setup 6 内置了对non-administrative install mode 的支持。
基本上,你可以简单地设置PrivilegesRequiredOverridesAllowed
:
[Setup]
PrivilegesRequiredOverridesAllowed=commandline dialog
此外,您可能希望使用常量的auto*
变体。尤其是autopf
的DefaultDirName
。
[Setup]
DefaultDirName=pf\My Program
以下是我的(现已过时的)Inno Setup 5 解决方案,基于 @TLama's answer。
当设置在非提升状态下启动时,它会请求提升,但有一些例外:
仅适用于 Windows Vista 和更新版本(尽管它也应该适用于 Windows XP) 升级时,安装程序将检查当前用户是否具有对先前安装位置的写入权限。如果用户具有写入权限,则安装程序不会请求提升。因此,如果用户之前已将应用程序安装到用户文件夹,则升级时不会请求提升。如果用户在新安装时拒绝提升,安装程序将自动回退到“本地应用程序数据”文件夹。 IE。 C:\Users\standard\AppData\Local\AppName
.
其他改进:
提升的实例不会再次要求语言 通过使用PrivilegesRequired=none
,安装程序将在提升时将卸载信息写入HKLM
,而不是HKCU
。
#define AppId "myapp"
#define AppName "MyApp"
#define InnoSetupReg \
"Software\Microsoft\Windows\CurrentVersion\Uninstall\" + AppId + "_is1"
#define InnoSetupAppPathReg "Inno Setup: App Path"
[Setup]
AppId=#AppId
PrivilegesRequired=none
...
[Code]
function IsWinVista: Boolean;
begin
Result := (GetWindowsVersion >= $06000000);
end;
function HaveWriteAccessToApp: Boolean;
var
FileName: string;
begin
FileName := AddBackslash(WizardDirValue) + 'writetest.tmp';
Result := SaveStringToFile(FileName, 'test', False);
if Result then
begin
Log(Format(
'Have write access to the last installation path [%s]', [WizardDirValue]));
DeleteFile(FileName);
end
else
begin
Log(Format('Does not have write access to the last installation path [%s]', [
WizardDirValue]));
end;
end;
procedure ExitProcess(uExitCode: UINT);
external 'ExitProcess@kernel32.dll stdcall';
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
lpParameters: string; lpDirectory: string; nShowCmd: Integer): THandle;
external 'ShellExecuteW@shell32.dll stdcall';
function Elevate: Boolean;
var
I: Integer;
RetVal: Integer;
Params: string;
S: string;
begin
Collect current instance parameters
for I := 1 to ParamCount do
begin
S := ParamStr(I);
Unique log file name for the elevated instance
if CompareText(Copy(S, 1, 5), '/LOG=') = 0 then
begin
S := S + '-elevated';
end;
Do not pass our /SL5 switch
if CompareText(Copy(S, 1, 5), '/SL5=') <> 0 then
begin
Params := Params + AddQuotes(S) + ' ';
end;
end;
... and add selected language
Params := Params + '/LANG=' + ActiveLanguage;
Log(Format('Elevating setup with parameters [%s]', [Params]));
RetVal :=
ShellExecute(0, 'runas', ExpandConstant('srcexe'), Params, '', SW_SHOW);
Log(Format('Running elevated setup returned [%d]', [RetVal]));
Result := (RetVal > 32);
if elevated executing of this setup succeeded, then...
if Result then
begin
Log('Elevation succeeded');
exit this non-elevated setup instance
ExitProcess(0);
end
else
begin
Log(Format('Elevation failed [%s]', [SysErrorMessage(RetVal)]));
end;
end;
procedure InitializeWizard;
var
S: string;
Upgrade: Boolean;
begin
Upgrade :=
RegQueryStringValue(HKLM, '#InnoSetupReg', '#InnoSetupAppPathReg', S) or
RegQueryStringValue(HKCU, '#InnoSetupReg', '#InnoSetupAppPathReg', S);
elevate
if not IsWinVista then
begin
Log(Format('This version of Windows [%x] does not support elevation', [
GetWindowsVersion]));
end
else
if IsAdminLoggedOn then
begin
Log('Running elevated');
end
else
begin
Log('Running non-elevated');
if Upgrade then
begin
if not HaveWriteAccessToApp then
begin
Elevate;
end;
end
else
begin
if not Elevate then
begin
WizardForm.DirEdit.Text := ExpandConstant('localappdata\#AppName');
Log(Format('Falling back to local application user folder [%s]', [
WizardForm.DirEdit.Text]));
end;
end;
end;
end;
【讨论】:
【参考方案2】:在 Inno Setup 的生命周期内,没有内置的方法来条件提升设置过程。但是,您可以使用runas
动词执行设置过程并杀死非提升的那个。我编写的脚本有点棘手,但显示了一种可能的方法。
警告:
这里使用的代码总是尝试执行提升的设置实例;没有检查是否确实需要海拔高度(如何决定是否需要海拔高度,请在单独的问题中选择询问)。此外,我目前无法判断进行这种手动提升是否安全。我不确定 Inno Setup 是否(或不会)以某种方式依赖 PrivilegesRequired
指令的值。最后,这个提升的东西应该只在相关的 Windows 版本上执行。此脚本中未对此进行检查:
[Setup]
AppName=My Program
AppVersion=1.5
DefaultDirName=pf\My Program
PrivilegesRequired=lowest
[Code]
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
type
HINSTANCE = THandle;
procedure ExitProcess(uExitCode: UINT);
external 'ExitProcess@kernel32.dll stdcall';
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
lpParameters: string; lpDirectory: string; nShowCmd: Integer): HINSTANCE;
external 'ShellExecute#AW@shell32.dll stdcall';
var
Elevated: Boolean;
PagesSkipped: Boolean;
function CmdLineParamExists(const Value: string): Boolean;
var
I: Integer;
begin
Result := False;
for I := 1 to ParamCount do
if CompareText(ParamStr(I), Value) = 0 then
begin
Result := True;
Exit;
end;
end;
procedure InitializeWizard;
begin
initialize our helper variables
Elevated := CmdLineParamExists('/ELEVATE');
PagesSkipped := False;
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
if we've executed this instance as elevated, skip pages unless we're
on the directory selection page
Result := not PagesSkipped and Elevated and (PageID <> wpSelectDir);
if we've reached the directory selection page, set our flag variable
if not Result then
PagesSkipped := True;
end;
function NextButtonClick(CurPageID: Integer): Boolean;
var
Params: string;
RetVal: HINSTANCE;
begin
Result := True;
if we are on the directory selection page and we are not running the
instance we've manually elevated, then...
if not Elevated and (CurPageID = wpSelectDir) then
begin
pass the already selected directory to the executing parameters and
include our own custom /ELEVATE parameter which is used to tell the
setup to skip all the pages and get to the directory selection page
Params := ExpandConstant('/DIR="app" /ELEVATE');
because executing of the setup loader is not possible with ShellExec
function, we need to use a WinAPI workaround
RetVal := ShellExecute(WizardForm.Handle, 'runas',
ExpandConstant('srcexe'), Params, '', SW_SHOW);
if elevated executing of this setup succeeded, then...
if RetVal > 32 then
begin
exit this non-elevated setup instance
ExitProcess(0);
end
else
executing of this setup failed for some reason; one common reason may
be simply closing the UAC dialog
begin
handling of this situation is upon you, this line forces the wizard
stay on the current page
Result := False;
and possibly show some error message to the user
MsgBox(Format('Elevating of this setup failed. Code: %d', [RetVal]),
mbError, MB_OK);
end;
end;
end;
【讨论】:
要决定是否需要提升进程,对于转换为 Inno Setup 的 Pascal 脚本来说有点复杂。最简单(在我看来最安全)的方法是尝试创建文件夹,或者如果已经存在,则在其中写入文件。在某些情况下,Program Files 子文件夹可能已授予写访问权限,或者非 Program Files 子文件夹未授予。但这确实是一个值得在不同问题中占有一席之地的话题。 感谢您的回答。我认为可以在进程中提升特权。但现在我发现它不是:***.com/questions/573086/… 不客气!好吧,即使在 Windows Installer 设置中,您也可以看到“向导窗口是如何重新创建的”,它实际上启动了一个新的提升进程并将向导移动到您使用被杀死的非提升进程所在的页面。 @TLama 这种方法的问题在于 InnoSetup 会将卸载信息写入 HKCU 而不是 HKLM...显然它是由PrivilegesRequired
控制的。有办法解决吗?见***.com/q/42655340/98713
感谢您的赏金和整体贡献,@Martin! [但我需要复习很多]以上是关于仅在需要时使 Inno Setup 安装程序请求权限提升的主要内容,如果未能解决你的问题,请参考以下文章