仅在需要时使 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* 变体。尤其是autopfDefaultDirName

[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 安装程序请求权限提升的主要内容,如果未能解决你的问题,请参考以下文章

如何从Inno Setup安装程序设置全局环境变量?

将功能执行添加到 inno setup 的安装程序进度中

inno setup相关

在安装过程中使用 Inno Setup 删除另一个未随 Inno Setup 安装的应用程序

inno setup 怎样检测程序已安装

Inno Setup Compiler打包需要管理员权限的程序