Tuesday, April 6, 2010

Passing parameters and retrieving results from inner/outer UAC processes in NSIS-based installer


Foreword

While creating NSIS-based installer for one of my projects I used UAC plugin in order to elevate the installation process to administrator privileges. Those who used UAC plugin know that it creates additional admin level process during initialization and installation continues to operate in it. But user level process is still available and might be used in order to perform some user-related operations like creation of shortcut on the Desktop, creating of user-related preferences, etc.

When executing some code segment from user level process via calling to UAC::ExecCodeSegment it's sometimes required to pass parameters and retrieve back results, so this article would give some insight into this problem and a ways of solving it.

Passing parameters to user level process during UAC::ExecCodeSegment call

It is quite simple to pass parameter from admin level process (also called 'inner') to the user level process (also called 'outer') - all we need to do is to call UAC::StackPush function which will place specified value into outer process's stack. In order to get passed parameter in user level process we might simply call Pop or Exch function which will retrieve passed value.

; Function which is executed from user level process
Function UserLevelFunction
    ; Getting passed parameter into $0 variable.
    ; By calling 'Exch' we might preserve value which already
    ; was stored in $0 (if not required a call to 'Pop' might be used).
    Exch $0

    ; Showing test message from user level process with retrieved parameter
    MessageBox MB_OK "Parameter: $0"

    ; Getting back previous value of $0 variable (if required)
    Pop $0
FunctionEnd

; Section which is executed from admin level process
Section "UACParameterPassingTest"
    ; Specifies for which install types this section would be used
    SectionIn 1 2

    ; Placing parameter into stack of user level process
    UAC::StackPush "testParameter"

    ; Executing function with user privileges
    GetFunctionAddress $1 UserLevelFunction
    UAC::ExecCodeSegment $1
SectionEnd

As you might have noticed there is nothing complex about passing parameters to the user level process so lets proceed to the next section as that one requires non-trivial solution.

Retrieving results from user level process after UAC::ExecCodeSegment call

Unfortunately there is no standard way of passing back results of execution from function called in outer process. There is also no possibility to create some global variable which would be shared between inner/outer processes and pass results via it. So during implementation of my installer I created one solution and am going to describe it in more details here.

The main idea of proposed solution is to write resulting data to some file in a system folder accessible from both user level and admin level processes. In case of my installer I used user's Application Data folder to store file in it. In order to retrieve path to this folder from both processes we need to define special item ID constant in installer's code (please refer to following article for more details: CSIDL Windows)

!define CSIDL_LOCAL_APPDATA "0x1C"

and call to UAC::GetShellFolderPath function

UAC::GetShellFolderPath ${CSIDL_LOCAL_APPDATA} $APPDATA

as this guarantees that we'll receive the same path for user and admin level processes. So, once we have strictly defined place for populating/reading results, the implementation of overall solution doesn't bring much trouble. The complete solution looks like following:

!define CSIDL_LOCAL_APPDATA "0x1C"

; Function which is executed from user level process
; and returns result to admin level process
Function UserLevelFunction
    ; $0 - contains path to user's local AppData folder
    ; $1 - contains temporary file handle

    ; Retrieving the path to user's Application Data folder.
    ; The result is automatically placed into $0 variable.
    UAC::GetShellFolderPath ${CSIDL_LOCAL_APPDATA} $APPDATA

    ; Writing result of execution to the file accessible
    ; by both user and admin processes.
    FileOpen $1 "$0\UACResultRetrievingTest.dat" w
    FileWrite $1 "functionResult"
    FileClose $1
FunctionEnd

; Function which is executed from admin level process
; and processes result retrieved from user level process
Function ProcessResult
    ; $0 - contains path to user's local AppData folder
    ; $1 - contains temporary file handle
    ; $2 - contains result of execution read from temporary file

    ; Retrieving the path to user's Application Data folder.
    ; The result is automatically placed into $0 variable.
    UAC::GetShellFolderPath ${CSIDL_LOCAL_APPDATA} $APPDATA

    ; Reading result of execution from the file accessible
    ; by both user and admin processes.
    FileOpen $1 "$0\UACResultRetrievingTest.dat" r
    FileRead $1 $2
    FileClose $1

    ; Removing temporary file (if required)
    Delete "$0\UACResultRetrievingTest.dat"

    MessageBox MB_OK "Result of execution: $2"
FunctionEnd

; Section which is executed from admin level process
Section "UACResultRetrievingTest"
    ; Specifies for which install types this section would be used
    SectionIn 1 2

    ; Executing function with user privileges
    GetFunctionAddress $0 UserLevelFunction
    UAC::ExecCodeSegment $0

    ; Executing function with admin privileges
    Call ProcessResult
SectionEnd

As you might see the solution is not complex one but it took a while to get to it. So hopefully it will help someone to save a bit of time for more interesting tasks.

Conclusion

Proposed techniques allow to pass required values between inner and outer processes in NSIS-based installer which uses UAC plugin for elevation to administrator privileges. As an example, I used this approach to pass user's registry SID retrieved in user level process but required in admin level process. There are few posts on NSIS Discussion forum proposing rare solutions for similar problem, but they are not well structured and not complete so as the result - hard to understand. This post intended to summaraze all those techniques and provide complete solution with code examples.