Background
We need to run a GUI application from a Windows Service, set to Log On as Local System (and without enabling interact with desktop).
The GUI application takes one command-line parameter, performs a specific task and then self-terminates. It is a GUI app because some of its components require a parent TForm
, so a console app doesn't work. There are no dialogs or any UI a user would see. In fact, it creates itself as a hidden form with no taskbar icon:
Application.Initialize;
Application.MainFormOnTaskbar := False; // <- No taskbar icon
Application.ShowMainForm := False; // <- Main form is hidden
Application.CreateForm(TForm1, Form1);
Application.Run;
It is possible that the GUI app may be launched multiple times simultaneously, each with its own command-line parameter. Since a GUI app can't be spawned directly in the Session 0 process of the service, I created an Administrator user account so the service can log on the admin user and run the GUI app as the admin user. Once I get it to work once, I will leave this user logged in so the service can quickly launch the GUI app without the login/logout overhead each time it spawns the GUI app.
What I've Done
I'm using the following code, formed from dozens of discussions on this topic, even though most of them wanted the GUI app to be seen by a logged on user.
function CreateEnvironmentBlock(var lpEnvironment: Pointer; hToken: THandle; bInherit: BOOL): BOOL; stdcall; external 'userenv.dll';
function DestroyEnvironmentBlock(lpEnvironment: Pointer): BOOL; stdcall; external 'userenv.dll';
var
_usertoken: THandle;
_si: _STARTUPINFOW;
_pi: _PROCESS_INFORMATION;
_env: Pointer;
_sid: Cardinal;
begin
if LogonUser(PChar(Username), PChar('localhost'), PChar(Password), LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, _usertoken) then
try
ZeroMemory(@_si, SizeOf(_si));
_si.cb := SizeOf(_si);
// _si.lpDesktop := 'WinSta0\Default'; // <- behaves the same with or without this
if CreateEnvironmentBlock(_env, _usertoken, False) then
try
if CreateProcessAsUser(_usertoken, nil, PChar(sCMD), nil, nil, False, CREATE_UNICODE_ENVIRONMENT, _env, nil, _si, _pi) then
begin
WaitForSingleObject(_pi.hProcess, 30000);
CloseHandle(_pi.hThread);
CloseHandle(_pi.hProcess);
end
else
_handle_error('CreateProcessAsUser() failed.');
finally
DestroyEnvironmentBlock(_env);
end
else
_handle_error('CreateEnvironmentBlock() failed.');
finally
CloseHandle(_usertoken);
end
else
_handle_error('LogonUser() failed.');
end;
The Windows Event Viewer [Security Log] shows an entry when LogonUser()
is called. The following privileges appear in the log entry:
SeTcbPrivilege
SeSecurityPrivilege
SeTakeOwnershipPrivilege
SeLoadDriverPrivilege
SeBackupPrivilege
SeRestorePrivilege
SeDebugPrivilege
SeSystemEnvironmentPrivilege
SeImpersonatePrivilege
sCmd
is set to "c:\path\myapp.exe" "parameter". When sCmd
was not properly set, CreateProcessAsUser()
would fail with an error of 2 - The system cannot find the file specified. Once I fixed that, CreateProcessAsUser()
returns True
, but it never actually launches the GUI application.
Question
I'm not sure what I'm missing. I would appreciate any help with getting the service to launch the GUI app under the logged on Username/Password profile, if that's the right way to do this. Or, if there is a better way to do it, I would appreciate any direction and insight.