Executing an EXE with Run As Administrator Dialog
August 29, 2007 •
With Windows Vista there are a number of things that require potential elevation of rights for applications in order to perform certain administrative tasks. For example in Help Builder the auto-updater requires that a new updated EXE can be written to the Program Files installation folder when an update becomes available.
Although this is a more acute issue with Vista, this also applies to XP if you are not running as an Administrator.
Two things make this process easier: A way to detect whether the user has Admin capabilities and a way to restart an application by prompting with the Run As Administrator dialog.
In Help Builder I have code that basically does the following (after checking for whether updates are actually available):
IF !IsAdmin()
GoUrl( GetAppStartPath(), "RUNAS")
QUIT
ENDIF
To force a restart. And it works well.
The first thing is figuring out whether you are an admin. There are several API calls available for this and this one’s the easiest and most widely available on Windows:
************************************************************************
* wwUtils :: IsAdmin
****************************************
*** Function: Determines whether user is an admin user
*** Assume:
*** Pass:
*** Return:
************************************************************************
FUNCTION IsAdmin()
LOCAL loAPI, lcVal
DECLARE INTEGER IsUserAnAdmin IN Shell32
TRY
lnResult = IsUserAnAdmin()
CATCH
*** OLD OLD Version of Windows assume .T.
lnResult = 1
ENDTRY
IF lnResult = 0
RETURN .F.
ENDIF
RETURN .T.
ENDFUNC
* wwUtils :: IsAdmin
Note although the docs say it works with Windows 2000 forward, the API call doesn’t exist in the original non-SP’d version of Win2000. The above API was introduced with one of the service packs. However, if the box is that old and unpatched most likely security is not going to be an issue so it just assumes the user is an admin.
Next you can use your application to figure out exactly what EXE what used to launch the application. I like to use a generic routine that figures out what actually launched an application:
************************************************************************
FUNCTION GetAppStartPath
*********************************
*** Function: Returns the FoxPro start path
*** of the *APPLICATION*
*** under all startmodes supported by VFP.
*** Returns the path of the starting EXE,
*** DLL, APP, PRG/FXP
*** Return: Path as a string with trailing "\"
************************************************************************
DO CASE
*** VFP 6 and later provides ServerName property for COM servers EXE/DLL/MTDLL
CASE INLIST(Application.StartMode,2,3,5)
lcPath = JustPath(Application.ServerName)
*** Interactive
CASE (Application.StartMode) = 0
lcPath = SYS(5) + CURDIR()
*** Active Document
CASE ATC(".APP",SYS(16,0)) > 0
lcPath = JustPath(SYS(16,0))
*** Standalone EXE or VFP Development
OTHERWISE
lcPath = JustPath(SYS(16,0))
IF ATC("PROCEDURE",lcPath) > 0
lcPath = SUBSTR(lcPath,RAT(":",lcPath)-1)
ENDIF
ENDCASE
RETURN AddBs(lcPath)
* EOF GetAppStartPath
Finally you can use ShellExecute with an action of RUNAS to force the Administrative rights dialog to pop up. Again I use a wrapper method:
****************************************************
FUNCTION GoUrl
******************
*** Author: Rick Strahl
*** (c) West Wind Technologies, 1996
*** Contact: rstrahl@west-wind.com
*** Modified: 03/14/96
*** Function: Starts associated Web Browser
*** and goes to the specified URL.
*** If Browser is already open it
*** reloads the page.
*** Assume: Works only on Win95 and NT 4.0
*** Pass: tcUrl - The URL of the site or
*** HTML page to bring up
*** in the Browser
*** Return: 2 - Bad Association (invalid URL)
*** 31 - No application association
*** 29 - Failure to load application
*** 30 - Application is busy
***
*** Values over 32 indicate success
*** and return an instance handle for
*** the application started (the browser)
****************************************************
LPARAMETERS tcUrl, tcAction, tcDirectory, tcParms
IF EMPTY(tcUrl)
RETURN -1
ENDIF
IF EMPTY(tcAction)
tcAction = "OPEN"
ENDIF
IF EMPTY(tcDirectory)
tcDirectory = SYS(2023)
ENDIF
DECLARE INTEGER ShellExecute ;
IN SHELL32.dll ;
INTEGER nWinHandle,;
STRING cOperation,;
STRING cFileName,;
STRING cParameters,;
STRING cDirectory,;
INTEGER nShowWindow
IF EMPTY(tcParms)
tcParms = ""
ENDIF
DECLARE INTEGER FindWindow ;
IN WIN32API ;
STRING cNull,STRING cWinName
RETURN ShellExecute(FindWindow(0,_SCREEN.caption),;
tcAction,tcUrl,;
tcParms,tcDirectory,1)
I’ve used this approach in several applications now and it works well. I use self-updating code in several applications and this fix – while not perfect – is acceptable. It’s reasonable IMHO to prompt people to restart with admin rights for an operation that copies new code onto your machine.
Just for fun here’s the whole Help Builder update routine. It uses the wwCodeUpdate component I described in an article some time ago.
************************************************************************
PROCEDURE CheckForNewVersion
****************************
*** Modified: 05/30/00
*** Function: Checks for a new version every few days
*** Pass: lnDays
*** Return: nothing
*************************************************************************
LPARAMETER lnDays, loConfig, llForceResult
LOCAL loUpdate, loIP
IF VARTYPE(loConfig) # "O"
loConfig = CREATE("wwHelpConfig")
ENDIF
IF EMPTY(lnDays)
lnDays = loConfig.nVersionCheckFrequency
ENDIF
*** Load the configuration settings from INI file
loConfig.Load()
IF !llForceResult AND loConfig.dLastVersionCheck > DATE() - lnDays
RETURN
ENDIF
*** Create HTTP Component and make sure server is accessible
*** wait for no more than 2 seconds
loIP = CREATE("wwHTTP")
IF !loIP.HTTPPing(3,HTTP_SERVER,WWHELP_DOWNLOADCHECK_LINK_RELATIVEURL)
IF llForceResult
MESSAGEBOX("Unable to connect to " + HTTP_SERVER,48,WWHELP_APPNAME)
ENDIF
RETURN
ENDIF
*** Now set up to download the servers version info file (XML)
loUpdate=CREATEOBJECT("wwCodeUpdate")
loUpdate.cVersionUrl = "http://"+HTTP_SERVER + WWHELP_DOWNLOADCHECK_LINK_RELATIVEURL
loUpdate.cVersionType = "N"
loUpdate.cEXEFile = "wwhelp.exe"
*** Retrieve the version and update info from the server
lnVersion = loUpdate.GetVersionInfo()
IF EMPTY(lnVersion)
IF llForceResult
MESSAGEBOX("There's been an error retrieving the version info.",48,WWHELP_APPNAME)
ENDIF
RETURN
ENDIF
*** Update the version check date in the INI (not saved yet)
loConfig.dLastVersionCheck = DATE()
loConfig.Save()
lnCurVersion = WWHELP_VERSION
IF !loUpdate.IsUpToDate(WWHELP_VERSION)
IF MessageBox(loUpdate.cUserMessage + CHR(13) +;
"Would you like to download and install it now?",;
32+4,;
"HTML Help Builder Update Notice") = 6
*** Admin Prompt
IF !IsAdmin()
TEXT TO RunAsAdminNote TEXTMERGE NOSHOW
<body style="font-family:verdana;font-size:10pt;top-margin:0" scroll="no">
<table><tr><td>
<img src="<< SYS(5)+CURDIR() >>bmp\images\alertIcon.gif" align="left" hspace="5">
<td><td align="center" valign="center">
<b style="font-size:12pt;color:maroon">Administrative Permissions Required</b>
</td><tr></table>
<hr style="color:darkblue;height:1pt;">
<p>
In order to download and install this Help Builder update you need to be
logged in as an Administrator.
<p>
You are currently <b>not logged</b> in as an Administrator.
<p>
To perform the Help Builder update, please exit the application
and restart it by using the Run As option and choosing a user
account that has Administrative priviliges from the
shortcut menu or from Windows Explorer (wwhelp.exe).
<p>
<b>What would you like to do?</b>
<ul><li> <b>Exit</b> Help Builder and show Help Builder install directory
<li> Just <b>Return</b> back to Help Builder
<li> Go ahead and <b>Try anyway</b> to update
<li> Get more information in the <b>Help</b> file
</ul>
</body>
ENDTEXT
lcResult = MessageDisplay(RunAsAdminNote,"Help Builder Update",;
"Exit,Return,Try anyway,Help",450,420,VARTYPE(goHelp) = "U")
DO CASE
CASE lcResult = 2 OR lcResult < 1
RETURN
CASE lcResult = 1
*** Update the version check date so it pops back up
loConfig.dLastVersionCheck = DATE() - 60
loConfig.Save()
IF (VARTYPE(GoHelp) = "O")
GoUrl(GetAppStartPath(),"RUNAS")
ENDIF
QUIT
CASE lcResult = 4
HELP ID 875
RETURN
ENDCASE
ENDIF && IsAdmin
*** End Admin Prompt
IF !loUpdate.DownloadUpdate(.F.)
MessageBox(loUpdate.cErrorMsg,48,"Download Error")
ELSE
*** Also try to download Updated CodeUpdate.exe
lcFile = loIP.HttpGet(WWHELP_DOWNLOADCODEUPDATE_LINK)
IF loIP.nError = 0 AND !EMPTY(lcFile)
FILE2Var(GetAppStartPath() + "codeupdate.exe",lcFile)
ENDIF
CLEAR EVENTS
*** Make sure we release
goHelp = .f.
*** Shut down Help Builder
IF TYPE("THISFORM") = "O"
RELEASE THISFORM
ENDIF
*** Erase the Menu file
ERASE "bmp\main.tb"
*** Make sure Help Builder Add-In isn't still loaded
IF FILE("vsAddin\HelpBuildervsAddin.dll")
lnHandle = FOPEN("vsAddin\HelpBuildervsAddin.dll",2)
IF lnHandle > -1
FCLOSE(lnHandle)
ELSE
MESSAGEBOX("Visual Studio .NET and the Help Builder add-in are still running."+ CHR(13) +;
"Please shut down all instances of VS.NET before continuing.",48,;
WWHELP_APPNAME)
ENDIF
ENDIF
*** Shuts down Help Builder, unzips the update file and then restarts
loUpdate.RunUpdateExe(SYS(5) + ADDBS(CURDIR()) + "CodeUpdate.exe",;
["wwHelp.exe","] + ;
SYS(5) + ADDBS(CURDIR()) +;
[codeupdate\wwHelp400_Update.exe /auto ] + SYS(5) + CURDIR() + [",] + ;
["West Wind HTML Help Builder"])
ENDIF
ENDIF
ELSE
IF llForceResult
MESSAGEBOX("Your version of Help Builder is up to date",64,WWHELP_APPNAME)
ENDIF
ENDIF
RETURN
* eof CheckfornewVersion
This approach uses a separate EXE to handle the update itself. So the main app runs as wwhelp.exe and it downloads the file and uses an external EXE only for the update.
Notice that if you’re running as an administrator any child processes launched from the main EXE also end up running under Admin priviliges so when the updater fires it properly updates the files.