Service Application in Delphi do not Open an Exe file on desktop? - delphi

I am using a Service Application in Delphi. This is my code in ServiceExecute method
procedure TMDPSERVICE.ServiceExecute(Sender: TService);
const
SecBetweenRuns = 10;
var
Count: Integer;
begin
while not Terminated do // loop around until we should stop
begin
Inc(Count);
if Count >= SecBetweenRuns then
begin
if not DoFindTask('c:\weblod.exe') then
WinExec(PAnsiChar('c:\weblod.exe'), SW_NORMAL);
end;
Sleep(10);
end;
end;
I can see weblod.exe file on taskmanager but it is not showing on Desktop?

Services run in an isolated session, session 0. Interactive desktops are always in different sessions. Read about session 0 isolation to learn more.
If you want to launch the process on an interactive desktop you need something like this: create process in user session from service

First and foremost, you are misusing the TService.OnExecute event. Specifically, you are never calling ServiceThread.ProcessRequests() so your service can respond to SCM requests. At the very least, you must add that to your loop:
procedure TMDPSERVICE.ServiceExecute(Sender: TService);
const
SecBetweenRuns = 10;
var
Count: Integer;
begin
while not Terminated do // loop around until we should stop
begin
Inc(Count);
if Count >= SecBetweenRuns then
begin
if not DoFindTask('c:\weblod.exe') then
WinExec(PAnsiChar('c:\weblod.exe'), SW_NORMAL);
end;
Sleep(10);
ServiceThread.ProcessRequests(False); // <-- add this
end;
end;
A better (and preferred) option is to not use the OnExecute event at all. TService automatically processes SCM requests when no OnExecute handler is assigned. You should use the TService.OnStart event to start a worker thread, and use the TService.OnStop/TService.OnShutdown events to terminate that thread:
type
TMyTaskThread = class(TThread)
protected
procedure Execute; override;
end;
procedure TMyTaskThread.Execute;
const
SecBetweenRuns = 10;
var
Count: Integer;
begin
while not Terminated do // loop around until we should stop
begin
Inc(Count);
if Count >= SecBetweenRuns then
begin
if not DoFindTask('c:\weblod.exe') then
WinExec(PAnsiChar('c:\weblod.exe'), SW_NORMAL);
end;
Sleep(10);
end;
end;
type
TMDPSERVICE = class(TService)
procedure ServiceStart(Sender: TService; var Started: Boolean);
procedure ServiceStop(Sender: TService; var Stopped: Boolean);
procedure ServiceShutdown(Sender: TService);
private
FTask: TMyTaskThread;
end;
procedure TMDPSERVICE.ServiceStart(Sender: TService; var Started: Boolean);
begin
FTask := TMyTaskThread.Create(False);
Started := True;
end;
procedure TMDPSERVICE.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
ServiceShutdown(Sender);
Stopped := True;
end;
procedure TMDPSERVICE.ServiceShutdown(Sender: TService);
begin
if Assigned(FTask) then
begin
FTask.Terminate;
while WaitForSingleObject(FTask.Handle, WaitHint-100) = WAIT_TIMEOUT do
ReportStatus;
FreeAndNil(FTask);
end;
end;
Now, with that said, WinExec() has been deprecated since Windows 95 was first introduced. Don't use WinExec() at all, especially from a service. You need to use CreateProcessAsUser() instead. Not only does that allow you to specify which desktop to run the process on, it also allows you to specify which user session the process runs in. This is especially important in Windows Vista and later due to Session 0 Isolation, because logged-in users do not run in the same session as services anymore (which is also why the TService.Interactive property is no longer supported, either). If you don't specify a user session, the process would run in the same session as the service, and as the same user that the service is running as (which is usually SYSTEM). A logged-in user will never see the process.
Try something more like this:
function WTSGetActiveConsoleSessionId: DWORD; stdcall; external 'Wtsapi32.dll';
function WTSQueryUserToken(SessionId: ULONG; var phToken: THandle): BOOL; stdcall; external 'Wtsapi32.dll';
function CreateEnvironmentBlock(var lpEnvironment: Pointer; hToken: THandle; bInherit: BOOL): BOOL; stdcall; external 'Userenv.dll';
function DestroyEnvironmentBlock(lpEnvironment: Pointer): BOOL; stdcall; external 'Userenv.dll';
function RunTaskOnUserDesktop(CmdLine: string): Boolean;
var
hToken: THandle;
env: Pointer;
si: STARTUPINFO;
pi: PROCESS_INFORMATION;
begin
Result := False;
// WTSGetActiveConsoleSessionId() returns the ID of the user session that is
// logged in to the physical console (keyboard/mouse/screen). If remote users
// can login to your machine, and you want to run your process in a remote
// user's session, use WTSEnumerateSessions() instead to find the ID of the
// desired logged-in user session...
//
if not WTSQueryUserToken(WTSGetActiveConsoleSessionId(), hToken) then
Exit;
try
if not CreateEnvironmentBlock(env, hToken, False) then
Exit;
try
ZeroMemory(#si, SizeOf(si));
si.cb := SizeOf(si);
si.lpDesktop := 'Winsta0\Default';
si.dwFlags := STARTF_USESHOWWINDOW;
si.wShowWindow := SW_SHOWNORMAL;
Result := CreateProcessAsUser(hToken, nil, PChar(CmdLine), nil, nil, False, CREATE_UNICODE_ENVIRONMENT, env, nil, si, pi);
if Result then
begin
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
end;
finally
DestroyEnvironmentBlock(env);
end;
finally
CloseHandle(hToken);
end;
end;
...
const
SecBetweenRuns = 10;
var
Count: Integer;
begin
while not Terminated do // loop around until we should stop
begin
Inc(Count);
if Count >= SecBetweenRuns then
begin
if not DoFindTask('c:\weblod.exe') then
RunTaskOnUserDesktop('c:\weblod.exe');
end;
Sleep(10);
end;
end;

Related

Console App and SetWindowsHookEx

Trying to setup a SetWindowsHookEx(WH_KEYBOARD) from a console app. I'm doing this inside thread, because I tried to to use the TThread.WaitFor method to keep the application openned, while the thread is running.
Important code parts:
type
THookKeyboard = procedure; stdcall;
KeyloggerThread = class(TThread)
private
const
MESSAGE_CODE = WM_USER + $1000;
var
HookOn, HookOff: THookKeyboard;
MsgReceptor: ^Integer;
MemFile: THandle;
function InstallKeyLogger(const TempDir: String): bool;
procedure HookMessage(var MessageHandler: TMessage); message MESSAGE_CODE;
protected
constructor Create;
procedure Execute; override;
end;
var
KeylogThreadCtrl: KeyloggerThread;
function KeyloggerThread.InstallKeyLogger(const TempDir: String): bool;
var
DLLHandle: THandle;
begin
Result:= false;
if FileExists(TempDir + DLLName) = true then
begin
DLLHandle:= LoadLibrary(PChar(TempDir + DLLName));
if DLLHandle <> 0 then
begin
#HookOn:= GetProcAddress(DLLHandle, 'HookOn');
#HookOff:= GetProcAddress(DLLHandle, 'HookOff');
end;
if assigned(HookOn) and assigned(HookOff) then
begin
MemFile:= CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0,SizeOf(Integer), 'Win32KLCom');
if MemFile <> 0 then
begin
MessageBox(0, 'starting keylogger', 'hook', MB_OK);
MsgReceptor:= MapViewOfFile(MemFile,FILE_MAP_WRITE,0,0,0);
HookOn;
Result:= true;
end;
end;
end;
end;
procedure KeyloggerThread.HookMessage(var MessageHandler: TMessage);
begin
MessageBox(0, 'pressed something!', 'hook', MB_OK);
end;
constructor KeyloggerThread.Create;
begin
inherited Create(false);
end;
procedure KeyloggerThread.Execute;
begin
while not Terminated do
begin
if not assigned(HookOn) then
if InstallKeyLogger(ExtractFilePath(ParamStr(0))) = false then
Terminate;
end;
end;
begin
if ParamStr(1) = '-runkeylog' then
begin
MessageBox(0, 'going to install keylogger', 'hook', MB_OK);
KeylogThreadCtrl:= KeyloggerThread.Create;
KeylogThreadCtrl.WaitFor;
end
end;
I know the InstallKeyLogger function is going fine, because I get the messagebox 'starting keylogger'.
Once I press any key, windows start freezing and I need to finish the application. The DLL code is:
library KeyboardDLL;
uses
Windows,
Messages;
{$R *.res}
const
MESSAGE_CODE = WM_USER + $1000;
var
KeyboardHook: HHook;
MemFile: THandle;
MsgReceptor: ^Integer;
function HookCallBack( Code : Integer;
wParam : WPARAM;
lParam : LPARAM
) : LRESULT; stdcall;
begin
if code=HC_ACTION then
begin
MemFile:= OpenFileMapping(FILE_MAP_WRITE,False, 'Win32KLCom');
if MemFile<>0 then
begin
MsgReceptor:= MapViewOfFile(MemFile,FILE_MAP_WRITE,0,0,0);
PostMessage(MsgReceptor^,MESSAGE_CODE,wParam,lParam);
end;
end;
Result:= CallNextHookEx(KeyboardHook, Code, wParam, lParam)
end;
procedure HookOn; stdcall;
begin
KeyboardHook:= SetWindowsHookEx(WH_KEYBOARD, #HookCallBack, HInstance , 0);
end;
procedure HookOff; stdcall;
begin
UnmapViewOfFile(MsgReceptor);
CloseHandle(MemFile);
UnhookWindowsHookEx(KeyboardHook);
end;
exports
HookOn,
HookOff;
begin
end.
It looks like you ported your hosting code from a VCL application, because you have some assumptions that don't hold for stand-alone threads, like the one you have there:
procedure HookMessage(var MessageHandler: TMessage); message MESSAGE_CODE;
Message procedures like this one only work in the context of a VCL form or control.
You can only post messages (use PostMessage) to window handles, not memory mapped files (as you attempt with the MsgReceptor pointer).
If you want your thread to be able to process messages, you must create a window handle and the thread must have a message loop (GetMessage/DispatchMessage, or similar).

Delphi and sleep function

I am having some issues regarding the sleep function. I have my application which executes an external command with some options:
str := 'C:\BERN52\MENU\menu.exe C:\BERN52\GPS\PAN\DAILY.INP C:\GPSUSER52\WORK\MENUAUX_DAILY.INP';
WinExec(Pansichar(str), SW_Shownormal);
After that when this process is finished I should kill it and continue with another things. I did the following:
Sleep(60000*StrToInt(Form1.Edit11.Text));
winexec('taskkill /F /IM menu.exe', SW_HIDE);
...
This sleeping time can be 4 minutes but also can be 2 days.
Of cause the main window going to the 'not responding' mode during this time. Could anyone suggest to me how to do this in a proper way?
First off, WinExec() has been deprecated since 32bit Windows was first introduced. Use ShellExecuteEx() or CreateProcess() instead. This also provides you with a process handle that you can use to detect when the spawned process terminates, and you can also use it to kill the process if your timeout elapses.
type
PHandle = ^THandle;
function StartProcess(const CmdLine: string; ProcessHandle: PHandle = nil): boolean;
var
si: TStartupInfo;
pi: TProcessInformation;
str: string;
begin
Result := False;
if ProcessHandle <> nil then ProcessHandle^ := 0;
str := CmdLine;
{$IFDEF UNICODE}
UniqueString(str);
{$ENDIF}
ZeroMemory(#si, sizeof(si));
si.cbSize := sizeof(si);
si.dwFlags := STARTF_USESHOWWINDOW;
si.wShowWindow := SW_SHOWNORMAL;
Result := CreateProcess(nil, PChar(str), nil, nil, False, 0, nil, nil, si, pi);
if Result then
begin
CloseHandle(pi.hThread);
if ProcessHandle <> nil then
ProcessHandle^ := pi.hProcess
else
CloseHandle(pi.hThread);
end;
end;
If you absolutely must block your calling code while waiting, use MsgWaitForMultipleObjects() in a loop so you can still service the message queue:
procedure TForm1.Start;
var
hProcess: THandle;
Timeout, StartTicks, Elapsed, Ret: DWORD;
begin
Timeout := 60000 * StrToInt(Edit11.Text);
if StartProcess('C:\BERN52\MENU\menu.exe C:\BERN52\GPS\PAN\DAILY.INP C:\GPSUSER52\WORK\MENUAUX_DAILY.INP', #hProcess) then
try
repeat
StartTicks := GetTickCount;
Ret := MsgWaitForMultipleObjects(1, hProcess, False, Timeout, QS_ALLINPUT);
if Ret <> (WAIT_OBJECT_0+1) then Break;
Application.ProcessMessages;
Elapsed := GetTickCount - StartTicks;
if Elapsed <= Timeout then
Dec(Timeout, Elapsed)
else
Timeout := 0;
until False;
if Ret <> WAIT_OBJECT_0 then
TerminateProcess(hProcess, 0);
finally
CloseHandle(hProcess);
end;
end;
Otherwise, use a TTimer so the main message loop is not blocked:
var
hProcess: THandle = 0;
procedure TForm1.Start;
begin
Timer1.Active := False;
if StartProcess('C:\BERN52\MENU\menu.exe C:\BERN52\GPS\PAN\DAILY.INP C:\GPSUSER52\WORK\MENUAUX_DAILY.INP', #hProcess) then
begin
Timer1.Tag := StrToInt(Edit11.Text);
Timer1.Interval := 1000;
Timer1.Active := True;
end;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
Ret: DWORD;
begin
Ret := WaitForSingleObject(hProcess, 0);
if Ret = WAIT_TIMEOUT then
begin
Timer1.Tag := Timer1.Tag - 1;
if Timer1.Tag > 0 then
Exit;
end;
if Ret <> WAIT_OBJECT_0 then
TerminateProcess(hProcess, 0);
CloseHandle(hProcess);
hProcess := 0;
Timer1.Active := False;
end;
Otherwise, use a worker thread instead of a timer:
type
TStartProcessThread = class(TThread)
private
fCmdLine: string;
fTimeout: DWORD;
fTermEvent: THandle;
protected
procedure Execute; override;
public
constructor Create(const CmdLine; Timeout: DWORD);
destructor Destroy; override;
procedure Stop;
end;
function StartProcess(const CmdLine: string; ProcessHandle: PHandle = nil): boolean;
begin
// as shown above...
end;
constructor TStartProcessThread.Create(const CmdLine; Timeout: DWORD);
begin
inherited Create(True);
fTermEvent := CreateEvent(nil, True, False, nil);
if fTermEvent = 0 then RaiseLastOSError;
fCmdLine := CmdLine;
fTimeout := Timeout;
FreeOnTerminate := True;
end;
destructor TStartProcessThread.Destroy;
begin
if fTermEvent <> 0 then CloseHandle(fTermEvent);
inherited;
end;
procedure TStartProcessThread.Stop;
begin
Terminate;
SetEvent(hTermEvent);
end;
procedure TStartProcessThread.Execute;
var
H: array[0..1] of THandle;
begin
if not StartProcess(fCmdLine, #H[0]) then Exit;
H[1] := fTermEvent;
if WaitForMultipleObjects(2, PWOHandleArray(#H), False, INFINITE) <> WAIT_OBJECT_0 then
TerminateProcess(H[0], 0);
CloseHandle(H[0]);
end;
var
Thread: TStartProcessThread = nil;
procedure TForm1.Start;
begin
Thread := TStartProcessThread.Create('C:\BERN52\MENU\menu.exe C:\BERN52\GPS\PAN\DAILY.INP C:\GPSUSER52\WORK\MENUAUX_DAILY.INP', 60000 * StrToInt(Edit11.Text));
Thread.OnTerminate := ThreadTerminated;
Thread.Start;
end;
procedure TForm1.ThreadTerminated(Sender: TObject);
begin
Thread := nil;
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if Thread <> nil then
begin
Thread.OnTerminate := nil;
Thread.Stop;
end;
end;
If you call Sleep in the UI thread, then the UI thread is no longer able to service its message queue. The not responding message is inevitable. The clear conclusion from this is that you must not call Sleep in the UI thread.
You could spin up another thread and put your Sleep call there. When that call to Sleep returns you can then do whatever needs to be done.
Some other comments:
Sleeping for such a long time is usually not the best solution to any problem. Perhaps you want to schedule a task. Or perhaps you'd be better having a periodic pulse in your program that checked whether the timeout had expired.
Winexec has been deprecated since 32 bit Windows was released, over 20 years. Use CreateProcess to start an external process.
If you wish to kill a process, use TerminateProcess.
Termination seems a little drastic. Isn't there any other way for you to persuade this other program to stop?

Call an external command in service [duplicate]

I've made a service with Delphi. Every time I call another application in that service the application is not running. What is wrong?
BTW I have used shellexecute, shellopen or calling it with cmd. None of these methods work.
This is my code:
program roro_serv;
uses
SvcMgr,
Unit1 in 'Unit1.pas' {Service1: TService},
ping in 'ping.pas';
{$R *.RES}
begin
Application.Initialize;
Application.CreateForm(TService1, Service1);
Application.Run;
end.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr, Dialogs,
ExtCtrls, DB, MemDS, DBAccess, MyAccess, Menus, forms, IniFiles,
ComCtrls, wininet, Variants, shellapi,
FileCtrl, ExtActns, StdCtrls, ShellCtrls;
type
TService1 = class(TService)
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject);
procedure ServiceExecute(Sender: TService);
procedure ServiceStop(Sender: TService; var Stopped: Boolean);
procedure ServiceStart(Sender: TService; var Started: Boolean);
private
{ Private declarations }
public
function GetServiceController: TServiceController; override;
{ Public declarations }
procedure run_procedure;
procedure log(text_file, atext : string );
procedure loginfo(text : string);
function CheckUrl(url: string): boolean;
procedure execCMD(CommandLine, Work: string);
function DoDownload(FromUrl, ToFile: String): boolean;
end;
var
Service1: TService1;
iTime : integer;
limit_time : integer = 2;
myini : TiniFile;
default_exe_path : string = '';
default_log_path : string = '';
appdir : String = '';
implementation
{$R *.DFM}
uses ping;
function TService1.CheckUrl(url: string): boolean;
var
hSession, hfile, hRequest: hInternet;
dwindex,dwcodelen :dword;
dwcode:array[1..20] of char;
res : pchar;
begin
if pos('http://',lowercase(url))=0 then
url := 'http://'+url;
Result := false;
hSession := InternetOpen('InetURL:/1.0',
INTERNET_OPEN_TYPE_PRECONFIG,nil, nil, 0);
if assigned(hsession) then
begin
hfile := InternetOpenUrl(
hsession,
pchar(url),
nil,
0,
INTERNET_FLAG_RELOAD,
0);
dwIndex := 0;
dwCodeLen := 10;
HttpQueryInfo(hfile, HTTP_QUERY_STATUS_CODE,
#dwcode, dwcodeLen, dwIndex);
res := pchar(#dwcode);
result:= (res ='200') or (res ='302');
if assigned(hfile) then
InternetCloseHandle(hfile);
InternetCloseHandle(hsession);
end;
end;
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
Service1.Controller(CtrlCode);
end;
function TService1.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
procedure TService1.Timer1Timer(Sender: TObject);
begin
iTime:=iTime+1;
if iTime=15 then // (limit_time*60) then
begin
itime:=1;
run_procedure;
end;
// loginfo('Defaultlog : '+default_log_path+'; exe : '+default_exe_path);
end;
procedure TService1.ServiceExecute(Sender: TService);
begin
Timer1.Enabled := True;
while not Terminated do
ServiceThread.ProcessRequests(True);
Timer1.Enabled := False;
end;
procedure TService1.run_procedure;
var
i : integer;
sUrl, sLogFile, sAction, sAct_param : String;
begin
for i:=0 to 20 do
begin
sLogFile:=default_log_path+myini.ReadString('logs', 'log_file'+intTostr(i), '');
if fileexists(slogfile) then
begin
loginfo(slogfile+' tersedia');
sAction:=myini.ReadString('logs', 'action'+intTostr(i), '');
if ((trim(sAction)<>'') and (fileexists(default_exe_path+sAction))) then
begin
// this line is don't work in servcie
ShellExecute(Application.Handle, 'open', 'c:\Windows\notepad.exe', nil, nil, SW_SHOWNORMAL);
sAct_param:=myini.ReadString('logs', 'action_prm'+intTostr(i), '');
// this line is don't work in servcie
execCMD(sAction+' '+sAct_param, default_exe_path);
loginfo(sAction+' '+sAct_param+' defpath : '+default_exe_path);
// this loginfo works
end;
end else
begin
end;
end;
end;
procedure TService1.log(text_file, atext: string);
var
logFile : TextFile;
begin
AssignFile(LogFile, text_file);
if FileExists(text_file) then
Append(LogFile) else rewrite(LogFile);
WriteLn(logFile, aText);
CloseFile(LogFile);
end;
procedure TService1.loginfo(text: string);
begin
log(ChangeFileExt(application.exename, '.log'), formatdateTime('dd-mm-yyyy hh:nn:ss ', now)+
text);
end;
procedure TService1.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
myini.Free;
end;
procedure TService1.execCMD(CommandLine, Work: string);
var
SA: TSecurityAttributes;
SI: TStartupInfo;
PI: TProcessInformation;
StdOutPipeRead, StdOutPipeWrite: THandle;
WorkDir: string;
begin
with SA do begin
nLength := SizeOf(SA);
bInheritHandle := True;
lpSecurityDescriptor := nil;
end;
CreatePipe(StdOutPipeRead, StdOutPipeWrite, #SA, 0);
try
with SI do
begin
FillChar(SI, SizeOf(SI), 0);
cb := SizeOf(SI);
dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
wShowWindow := SW_HIDE;
hStdInput := GetStdHandle(STD_INPUT_HANDLE); // don't redirect stdin
hStdOutput := StdOutPipeWrite;
hStdError := StdOutPipeWrite;
end;
WorkDir := Work;
CreateProcess(nil, PChar('cmd.exe /C ' + CommandLine),
nil, nil, True, 0, nil,
PChar(WorkDir), SI, PI);
CloseHandle(StdOutPipeWrite);
finally
CloseHandle(StdOutPipeRead);
end;
end;
procedure TService1.ServiceStart(Sender: TService; var Started: Boolean);
begin
appdir:=ExtractFileDir(Application.ExeName);
myini:=TiniFile.Create(ExtractFileDir(application.ExeName)+'\setting.ini');
limit_time:=myini.ReadInteger('setting', 'limit_time', 0);
default_exe_path:=myini.ReadString('setting', 'default_exe_path','');
if trim(default_exe_path)='' then default_exe_path:=appdir+'\';
default_log_path:=myini.ReadString('setting', 'default_log_path','');
if trim(default_log_path)='' then default_log_path:=appdir+'\logs\';
end;
function TService1.DoDownload(FromUrl, ToFile: String): boolean;
begin
{ with TDownloadURL.Create(self) do
try
URL:=FromUrl;
FileName := ToFile;
ExecuteTarget(nil) ;
finally
Free;
end; }
end;
end.
Please see run_procedure code line;
Put simply: how can I call another application from my service?
ShellExecute/Ex() and CreateProcess() run the specified file/app in the same session as the calling process. A service always runs in session 0.
In XP and earlier, the first user to log in also runs in session 0, so a service can run an interactive process and have it viewable to that interactive user, but only if the service is marked as interactive (the TService.Interactive property is true). If multiple users are logged in, they run in session 1+, and thus cannot see interactive processes run by services.
Windows Vista introduced a new feature called "Session 0 Isolation". Interactive users no longer run in session 0 at all, they always run in session 1+ instead, and session 0 is not interactive at all (the TService.Interactive property no longer has any effect). However, to help with migration of legacy services, if a service runs an interactive process that tries to display a GUI on session 0, Windows prompts the current logged in user, if any, to switch to a separate desktop that temporarily makes the GUI viewable. In Windows 7 onwards, that legacy support is now gone.
In all versions on Windows from 2000 onwards, the correct way to run an interactive process from a service and have it be viewable to an interactive user is to use CreateProcessAsUser() to run the new process in the specified user's session and desktop. There are plenty of detailed examples available on MSDN, StackOverflow, and throughout the Web, so I'm not going to reiterate them here.
Services run in a different session from the interactive user. Services run in session 0. Session 0 processes do not have access to the interactive desktop. Which means that any attempt to show an interactive process in session 0 is doomed to fail. You are attempting to create a Notepad process which is interactive.
There are ways to launch a process on an interactive desktop from a session: Launching an interactive process from Windows Service in Windows Vista and later. As you will understand after reading that article, what you are attempting to do is non-trivial.
This solution is intended to be used from within a service, I thought I'd paste this code here as it was how I got my service to run an application as the currently logged in user.
function WTSQueryUserToken(SessionId: ULONG; var phToken: THandle): BOOL; stdcall; external 'Wtsapi32.dll';
procedure runApp(appName: String);
var
hToken: THandle;
StartupInfo: TStartupInfo;
ProcessInfo: TProcessInformation;
res: boolean;
begin
GetStartupInfo(StartupInfo);
if WTSQueryUserToken(WtsGetActiveConsoleSessionID, hToken) then
begin
res := CreateProcessAsUser(hToken, PWideChar(appName), nil, nil, nil, False, CREATE_NEW_CONSOLE, nil, nil, StartupInfo, ProcessInfo);
if res then
WaitForSingleObject(ProcessInfo.hProcess,INFINITE);
end;
end;
//Anywhere in your service or app
RunApp ('notepad.exe');

How to detect changes to files recursively?

I'm working on a multi-threaded component to load and manage a music library, and I have a property defining multiple root directories to include. One thread searches those directories for media files, adds/removes as necessary, and another thread goes through those files and fills in the ID3v2 tag information. I already have a mechanism to detect added/removed files, but I don't know how to detect changes.
How can I detect when changes have been made to any of these files from other outside applications? I'd like an instantaneous response and not have to wait for a thread to get to that file. Is there a way I can receive alerts when any files have been changed in any of these folders recursively?
The function that you need to use is ReadDirectoryChangesW. This is not the easiest function in the world to use and it is worth pointing out that it is not 100% reliable. It will sometimes fail to notify you of modifications. In my experience that is more likely to happen for shares.
This API can be used in synchronous or asynchronous modes. As is always the case, the synchronous version is much easier to code against. But of course it blocks the calling thread. So the way out of that is to put the calls to ReadDirectoryChangesW in different threads. If you have a very large number of directories to watch, then one watching thread per directory is going to be an unworkable burden. If that is so then you would need to grapple with asynchronous usage.
You bWatchSubtree parameter allows you to monitor an entire tree of directories which I think is what you want to do.
For more details I refer you to this article: Understanding ReadDirectoryChangesW.
Try this:
uses
ShlObj, ActiveX;
const
FILE_LIST_DIRECTORY = $0001;
cDir = 'E:\...'; // The directory to monitor
Type
PFileNotifyInformation = ^TFileNotifyInformation;
TFileNotifyInformation = Record
NextEntryOffset: DWORD;
Action: DWORD;
FileNameLength: DWORD;
FileName: Array[0..0] of WideChar;
End;
type
TWaitThread = class(TThread)
private
FForm: TMainForm;
procedure HandleEvent;
protected
procedure Execute; override;
public
constructor Create(Form: TMainForm);
Procedure SendFtp(F: String; AddIfError: Boolean);
end;
procedure TWaitThread.HandleEvent;
Var
FileOpNotification: PFileNotifyInformation;
Offset: Longint;
F: String;
AList: TStringList;
I: Integer;
begin
AList := TStringList.Create;
Try
With FForm Do
Begin
Pointer(FileOpNotification) := #FNotificationBuffer[0];
Repeat
Offset := FileOpNotification^.NextEntryOffset;
//lbEvents.Items.Add(Format(SAction[FileOpNotification^.Action], [WideCharToString(#(FileOpNotification^.FileName))]));
F := cDir + WideCharToString(#(FileOpNotification^.FileName));
if AList.IndexOf(F) < 0 Then
AList.Add(F);
PChar(FileOpNotification) := PChar(FileOpNotification)+Offset;
Until Offset=0;
For I := 0 To AList.Count -1 Do
// do whatever you need
End;
Finally
AList.Free;
End;
end;
constructor TWaitThread.Create(Form: TMainForm);
begin
inherited Create(True);
FForm := Form;
FreeOnTerminate := False;
end;
procedure TWaitThread.Execute;
Var
NumBytes: DWORD;
CompletionKey: DWORD;
begin
While Not Terminated Do
Begin
GetQueuedCompletionStatus( FForm.FCompletionPort, numBytes, CompletionKey, FForm.FPOverlapped, INFINITE);
if CompletionKey <> 0 Then
Begin
Synchronize(HandleEvent);
With FForm do
begin
FBytesWritten := 0;
ZeroMemory(#FNotificationBuffer, SizeOf(FNotificationBuffer));
ReadDirectoryChanges(FDirectoryHandle, #FNotificationBuffer, SizeOf(FNotificationBuffer), False, FNotifyFilter, #FBytesWritten, #FOverlapped, nil);
End;
End
Else
Terminate;
End;
end;
{MainForm}
private
FDirectoryHandle: THandle;
FNotificationBuffer: array[0..4096] of Byte;
FWatchThread: TThread;
FNotifyFilter: DWORD;
FOverlapped: TOverlapped;
FPOverlapped: POverlapped;
FBytesWritten: DWORD;
FCompletionPort: THandle;
procedure TMainForm.FormCreate(Sender: TObject);
begin
FCompletionPort := 0;
FDirectoryHandle := 0;
FPOverlapped := #FOverlapped;
ZeroMemory(#FOverlapped, SizeOf(FOverlapped));
Start;
end;
procedure TMainForm.Start;
begin
FNotifyFilter := 0;
FNotifyFilter := FNotifyFilter or FILE_NOTIFY_CHANGE_FILE_NAME;
FDirectoryHandle := CreateFile(cDir,
FILE_LIST_DIRECTORY,
FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,
Nil,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS or FILE_FLAG_OVERLAPPED,
0);
if FDirectoryHandle = INVALID_HANDLE_VALUE Then
Begin
Beep;
FDirectoryHandle := 0;
ShowMessage(SysErrorMessage(GetLastError));
Exit;
End;
FCompletionPort := CreateIoCompletionPort(FDirectoryHandle, 0, Longint(pointer(self)), 0);
ZeroMemory(#FNotificationBuffer, SizeOf(FNotificationBuffer));
FBytesWritten := 0;
if Not ReadDirectoryChanges(FDirectoryHandle, #FNotificationBuffer, SizeOf(FNotificationBuffer), False, FNotifyFilter, #FBytesWritten, #FOverlapped, Nil) Then
Begin
CloseHandle(FDirectoryHandle);
FDirectoryHandle := 0;
CloseHandle(FCompletionPort);
FCompletionPort := 0;
ShowMessage(SysErrorMessage(GetLastError));
Exit;
End;
FWatchThread := TWaitThread.Create(self);
TWaitThread(FWatchThread).Resume;
end;
procedure TMainForm.Stop;
begin
if FCompletionPort = 0 Then
Exit;
PostQueuedCompletionStatus(FCompletionPort, 0, 0, Nil);
FWatchThread.WaitFor;
FWatchThread.Free;
CloseHandle(FDirectoryHandle);
FDirectoryHandle := 0;
CloseHandle(FCompletionPort);
FCompletionPort := 0;
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
Stop;
end;

How can I call another application from my Delphi service?

I've made a service with Delphi. Every time I call another application in that service the application is not running. What is wrong?
BTW I have used shellexecute, shellopen or calling it with cmd. None of these methods work.
This is my code:
program roro_serv;
uses
SvcMgr,
Unit1 in 'Unit1.pas' {Service1: TService},
ping in 'ping.pas';
{$R *.RES}
begin
Application.Initialize;
Application.CreateForm(TService1, Service1);
Application.Run;
end.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr, Dialogs,
ExtCtrls, DB, MemDS, DBAccess, MyAccess, Menus, forms, IniFiles,
ComCtrls, wininet, Variants, shellapi,
FileCtrl, ExtActns, StdCtrls, ShellCtrls;
type
TService1 = class(TService)
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject);
procedure ServiceExecute(Sender: TService);
procedure ServiceStop(Sender: TService; var Stopped: Boolean);
procedure ServiceStart(Sender: TService; var Started: Boolean);
private
{ Private declarations }
public
function GetServiceController: TServiceController; override;
{ Public declarations }
procedure run_procedure;
procedure log(text_file, atext : string );
procedure loginfo(text : string);
function CheckUrl(url: string): boolean;
procedure execCMD(CommandLine, Work: string);
function DoDownload(FromUrl, ToFile: String): boolean;
end;
var
Service1: TService1;
iTime : integer;
limit_time : integer = 2;
myini : TiniFile;
default_exe_path : string = '';
default_log_path : string = '';
appdir : String = '';
implementation
{$R *.DFM}
uses ping;
function TService1.CheckUrl(url: string): boolean;
var
hSession, hfile, hRequest: hInternet;
dwindex,dwcodelen :dword;
dwcode:array[1..20] of char;
res : pchar;
begin
if pos('http://',lowercase(url))=0 then
url := 'http://'+url;
Result := false;
hSession := InternetOpen('InetURL:/1.0',
INTERNET_OPEN_TYPE_PRECONFIG,nil, nil, 0);
if assigned(hsession) then
begin
hfile := InternetOpenUrl(
hsession,
pchar(url),
nil,
0,
INTERNET_FLAG_RELOAD,
0);
dwIndex := 0;
dwCodeLen := 10;
HttpQueryInfo(hfile, HTTP_QUERY_STATUS_CODE,
#dwcode, dwcodeLen, dwIndex);
res := pchar(#dwcode);
result:= (res ='200') or (res ='302');
if assigned(hfile) then
InternetCloseHandle(hfile);
InternetCloseHandle(hsession);
end;
end;
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
Service1.Controller(CtrlCode);
end;
function TService1.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
procedure TService1.Timer1Timer(Sender: TObject);
begin
iTime:=iTime+1;
if iTime=15 then // (limit_time*60) then
begin
itime:=1;
run_procedure;
end;
// loginfo('Defaultlog : '+default_log_path+'; exe : '+default_exe_path);
end;
procedure TService1.ServiceExecute(Sender: TService);
begin
Timer1.Enabled := True;
while not Terminated do
ServiceThread.ProcessRequests(True);
Timer1.Enabled := False;
end;
procedure TService1.run_procedure;
var
i : integer;
sUrl, sLogFile, sAction, sAct_param : String;
begin
for i:=0 to 20 do
begin
sLogFile:=default_log_path+myini.ReadString('logs', 'log_file'+intTostr(i), '');
if fileexists(slogfile) then
begin
loginfo(slogfile+' tersedia');
sAction:=myini.ReadString('logs', 'action'+intTostr(i), '');
if ((trim(sAction)<>'') and (fileexists(default_exe_path+sAction))) then
begin
// this line is don't work in servcie
ShellExecute(Application.Handle, 'open', 'c:\Windows\notepad.exe', nil, nil, SW_SHOWNORMAL);
sAct_param:=myini.ReadString('logs', 'action_prm'+intTostr(i), '');
// this line is don't work in servcie
execCMD(sAction+' '+sAct_param, default_exe_path);
loginfo(sAction+' '+sAct_param+' defpath : '+default_exe_path);
// this loginfo works
end;
end else
begin
end;
end;
end;
procedure TService1.log(text_file, atext: string);
var
logFile : TextFile;
begin
AssignFile(LogFile, text_file);
if FileExists(text_file) then
Append(LogFile) else rewrite(LogFile);
WriteLn(logFile, aText);
CloseFile(LogFile);
end;
procedure TService1.loginfo(text: string);
begin
log(ChangeFileExt(application.exename, '.log'), formatdateTime('dd-mm-yyyy hh:nn:ss ', now)+
text);
end;
procedure TService1.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
myini.Free;
end;
procedure TService1.execCMD(CommandLine, Work: string);
var
SA: TSecurityAttributes;
SI: TStartupInfo;
PI: TProcessInformation;
StdOutPipeRead, StdOutPipeWrite: THandle;
WorkDir: string;
begin
with SA do begin
nLength := SizeOf(SA);
bInheritHandle := True;
lpSecurityDescriptor := nil;
end;
CreatePipe(StdOutPipeRead, StdOutPipeWrite, #SA, 0);
try
with SI do
begin
FillChar(SI, SizeOf(SI), 0);
cb := SizeOf(SI);
dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
wShowWindow := SW_HIDE;
hStdInput := GetStdHandle(STD_INPUT_HANDLE); // don't redirect stdin
hStdOutput := StdOutPipeWrite;
hStdError := StdOutPipeWrite;
end;
WorkDir := Work;
CreateProcess(nil, PChar('cmd.exe /C ' + CommandLine),
nil, nil, True, 0, nil,
PChar(WorkDir), SI, PI);
CloseHandle(StdOutPipeWrite);
finally
CloseHandle(StdOutPipeRead);
end;
end;
procedure TService1.ServiceStart(Sender: TService; var Started: Boolean);
begin
appdir:=ExtractFileDir(Application.ExeName);
myini:=TiniFile.Create(ExtractFileDir(application.ExeName)+'\setting.ini');
limit_time:=myini.ReadInteger('setting', 'limit_time', 0);
default_exe_path:=myini.ReadString('setting', 'default_exe_path','');
if trim(default_exe_path)='' then default_exe_path:=appdir+'\';
default_log_path:=myini.ReadString('setting', 'default_log_path','');
if trim(default_log_path)='' then default_log_path:=appdir+'\logs\';
end;
function TService1.DoDownload(FromUrl, ToFile: String): boolean;
begin
{ with TDownloadURL.Create(self) do
try
URL:=FromUrl;
FileName := ToFile;
ExecuteTarget(nil) ;
finally
Free;
end; }
end;
end.
Please see run_procedure code line;
Put simply: how can I call another application from my service?
ShellExecute/Ex() and CreateProcess() run the specified file/app in the same session as the calling process. A service always runs in session 0.
In XP and earlier, the first user to log in also runs in session 0, so a service can run an interactive process and have it viewable to that interactive user, but only if the service is marked as interactive (the TService.Interactive property is true). If multiple users are logged in, they run in session 1+, and thus cannot see interactive processes run by services.
Windows Vista introduced a new feature called "Session 0 Isolation". Interactive users no longer run in session 0 at all, they always run in session 1+ instead, and session 0 is not interactive at all (the TService.Interactive property no longer has any effect). However, to help with migration of legacy services, if a service runs an interactive process that tries to display a GUI on session 0, Windows prompts the current logged in user, if any, to switch to a separate desktop that temporarily makes the GUI viewable. In Windows 7 onwards, that legacy support is now gone.
In all versions on Windows from 2000 onwards, the correct way to run an interactive process from a service and have it be viewable to an interactive user is to use CreateProcessAsUser() to run the new process in the specified user's session and desktop. There are plenty of detailed examples available on MSDN, StackOverflow, and throughout the Web, so I'm not going to reiterate them here.
Services run in a different session from the interactive user. Services run in session 0. Session 0 processes do not have access to the interactive desktop. Which means that any attempt to show an interactive process in session 0 is doomed to fail. You are attempting to create a Notepad process which is interactive.
There are ways to launch a process on an interactive desktop from a session: Launching an interactive process from Windows Service in Windows Vista and later. As you will understand after reading that article, what you are attempting to do is non-trivial.
This solution is intended to be used from within a service, I thought I'd paste this code here as it was how I got my service to run an application as the currently logged in user.
function WTSQueryUserToken(SessionId: ULONG; var phToken: THandle): BOOL; stdcall; external 'Wtsapi32.dll';
procedure runApp(appName: String);
var
hToken: THandle;
StartupInfo: TStartupInfo;
ProcessInfo: TProcessInformation;
res: boolean;
begin
GetStartupInfo(StartupInfo);
if WTSQueryUserToken(WtsGetActiveConsoleSessionID, hToken) then
begin
res := CreateProcessAsUser(hToken, PWideChar(appName), nil, nil, nil, False, CREATE_NEW_CONSOLE, nil, nil, StartupInfo, ProcessInfo);
if res then
WaitForSingleObject(ProcessInfo.hProcess,INFINITE);
end;
end;
//Anywhere in your service or app
RunApp ('notepad.exe');

Resources