【问题标题】:Make Inno Setup installer request privileges elevation only when needed仅在需要时使 Inno Setup 安装程序请求权限提升
【发布时间】: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 的支持。

【问题讨论】:

    标签: inno-setup uac privileges pascalscript elevated-privileges


    【解决方案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 子文件夹未授予。但这确实是一个值得在不同问题中占有一席之地的话题。
      • 感谢您的回答。我认为可以在进程中提升特权。但现在我发现它不是:stackoverflow.com/questions/573086/…
      • 不客气!好吧,即使在 Windows Installer 设置中,您也可以看到“向导窗口是如何重新创建的”,它实际上启动了一个新的提升进程并将向导移动到您使用被杀死的非提升进程所在的页面。
      • @TLama 这种方法的问题在于 InnoSetup 会将卸载信息写入 HKCU 而不是 HKLM...显然它是由PrivilegesRequired 控制的。有办法解决吗?见stackoverflow.com/q/42655340/98713
      • 感谢您的赏金和整体贡献,@Martin! [但我需要复习很多]
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-09
      • 1970-01-01
      • 1970-01-01
      • 2020-06-11
      • 2016-06-05
      • 1970-01-01
      相关资源
      最近更新 更多