Runshell in service.exs

Started by etippelt, June 13, 2014, 07:05:07 AM

Previous topic - Next topic

etippelt

Are there any known gotchas with using the Runshell command in a service running as the system account on windows 7 64 bit, using the 64 bit compiler?

Here is the code I am using:

DebugTrace(@on,"c:\si\trace.txt")
;Sample service script template

;Initialze variables for the SvcSetAccept function
SERVICE_ACCEPT_STOP             =   1    ;The service can be stopped
SERVICE_ACCEPT_PAUSE_CONTINUE   =   2    ;The service can be paused and continued
SERVICE_ACCEPT_SHUTDOWN         =   4    ;The service is notified when system shutdown occurs
SERVICE_ACCEPT_LOGOFF           =   32768;The service is notified when user logoff occurs

;Initialize variables for the SvcSetState function
SERVICE_STATE_STOPPED           =   1   ;The service is not running
SERVICE_STATE_START_PENDING     =   2   ;The service is initializing
SERVICE_STATE_STOP_PENDING      =   3   ;The service is stopping
SERVICE_STATE_RUNNING           =   4   ;The service is running
SERVICE_STATE_CONTINUE_PENDING  =   5   ;The service continue is pending
SERVICE_STATE_PAUSE_PENDING     =   6   ;The service pause is pending
SERVICE_STATE_PAUSED            =   7   ;The service is paused

;Initialize variables for the SvcWaitForCmd function
SERVICE_CONTROL_NOT_SERVICE     =  -1   ;Script not running as a service
SERVICE_CONTROL_TIMEOUT         =   0   ;Timeout occurred or no codes to process
SERVICE_CONTROL_STOP            =   1   ;Requests the service to stop
SERVICE_CONTROL_PAUSE           =   2   ;Requests the service to pause
SERVICE_CONTROL_CONTINUE        =   3   ;Requests the paused service to resume
SERVICE_CONTROL_SHUTDOWN        =   5   ;Requests the service to perform

                                        ;cleanup tasks, because the system is shutting down                                        ;is shutting down

SERVICE_CONTROL_USER128         = 128   ;User command 128
SERVICE_CONTROL_USER129         = 129   ;User command 129
SERVICE_CONTROL_USER130         = 130   ;User command 130
SERVICE_CONTROL_USER131         = 131   ;User command 131

;                                       ;More user commands as needed
SERVICE_CONTROL_USER255         = 255   ;User command 255
SERVICE_CONTROL_LOGOFF          = 32768 ;logoff notification

;Create working directory if it does not exist
if !DirExist("c:\SI") then  DirMake("c:\SI")
inicontrolfile="c:\SI\SI.ini"
;As directory had to be created then create the outline SI.ini file
IniWritePvt("Control","Request",0,inicontrolfile)   ; 0=No Request
IniWritePvt("Control","RunEXE","",inicontrolfile)    ; ""=No Action
IniWritePvt("Control","RunPath","",inicontrolfile)    ; ""=No Action
IniWritePvt("Control","RunString","",inicontrolfile)    ; ""=No Action
IniWritePvt("Control","ExitCode","",inicontrolfile)    ; ""=No Exit Code
IniWritePvt("Control","ServiceError","",inicontrolfile) ; ""=No Service Error
IniWritePvt("Debug","iniexe","",inicontrolfile)
IniWritePvt("Debug","inicmd","",inicontrolfile)
IniWritePvt("Debug","inistring","",inicontrolfile)
IniWritePvt("Debug","inipath","",inicontrolfile)

;Setup debugging prompt strings....
debugcodes="0: Timeout|1: Stop|2: Pause|3: Continue|5: Shutdown|128: User Cmd 128|129:User Cmd 129|32768: Logoff"

;Tell system that we want all notifications
flag=svcSetAccept( SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_LOGOFF )
if flag== -1
   DoingDebug=@TRUE
   Pause("Debug Mode","Not currently running as a service")
else
   DoingDebug=@FALSE
   ;Set up error handling
   IntControl(12,2+8,0,0,0)                    ;Tell WinBatch to not honor terminate and
                                               ;not complain on Windows exit
   IntControl(38,1,"C:\SI\errorlog.txt",0,0)   ;Route fatal errors to a log file
endif

;Now for the main service loop
;in this service we respond to all control messages
;and check an ini file for work every 5 seconds
;BoxOpen("Initializing","main service loop")

SvcSetState(SERVICE_STATE_RUNNING)

while @TRUE
   if DoingDebug==@FALSE
      code=SvcWaitForCmd(5000)      ; Timeout in 5 seconds
   else
       ;For Debugging.  Prompt tester to see what code should be presented here
       code=AskItemList("Service Debug",debugcodes,"|",@UNSORTED,@SINGLE)
       if code=="" then continue
       code=ItemExtract(1,code,":")
   endif

   switch code

          case SERVICE_CONTROL_TIMEOUT       
               ;Timeout occurred
               ;BoxText("TimeoutOccured")

               ;Read INI values just in case a request is detected
               inivalue=IniReadPvt("Control","Request",0,inicontrolfile)
               inipath=IniReadPvt("Control","RunPath","",inicontrolfile)
               inistring=IniReadPvt("Control","RunString","",inicontrolfile)
               iniexe=IniReadPvt("Control","RunEXE","",inicontrolfile)

               if inivalue!=0     ;Values have been read, an action has been requested, so reset back to starting values
                  IniWritePvt("Control","Request",0,inicontrolfile)
                  IniWritePvt("Control","RunPath","",inicontrolfile)
                  IniWritePvt("Control","RunString","",inicontrolfile)
                  IniWritePvt("Control","RunEXE","",inicontrolfile)
                  IniWritePvt("Control","ExitCode","",inicontrolfile)
                  IniWritePvt("Control","ServiceError","",inicontrolfile)  ;May not be necessary when more error handling is added
                  gosub DoIniWork
               endif
               break

          case SERVICE_CONTROL_STOP         
               ;Stop command received
               ;BoxText("Stop command received")
               SvcSetState(SERVICE_STATE_STOP_PENDING)
               ;do stop cleanup work here

               timedelay(5)
               SvcSetState(SERVICE_STATE_STOPPED)
               exit    ;Goodbye
               break

          case SERVICE_CONTROL_PAUSE         
               ;Pause command received
               ;BoxText("Pause command received")
               SvcSetState(SERVICE_STATE_PAUSE_PENDING)

               ;do pause cleanup work here
               SvcSetState(SERVICE_STATE_PAUSED)
               break

          case SERVICE_CONTROL_CONTINUE
               ;Continue command received
               ;BoxText("Continue command received")
               SvcSetState(SERVICE_STATE_CONTINUE_PENDING)
               ;do resume from pause state initilization here
               SvcSetState(SERVICE_STATE_RUNNING)
               break

          case SERVICE_CONTROL_SHUTDOWN     
               ;Shutdown notification received
               ;Approx. 20 seconds to process
               ;BoxText("Shutdown notification received")

               SvcSetState(SERVICE_STATE_STOP_PENDING)
               ;do stop cleanup work here
               SvcSetState(SERVICE_STATE_STOPPED)
               exit    ;Goodbye
               break

          case SERVICE_CONTROL_USER128       

               ;User command 128 received
               ;BoxText("User command 128 received")
               break

          case SERVICE_CONTROL_USER129
               ;User command 129 received
               ;BoxText("User command 129 received")
               break
   
          case SERVICE_CONTROL_LOGOFF
               ;Logoff command received
               ;BoxText("Logoff command received")
               break

         case code
               ;Unrecognized command received
               ;BoxText("Unrecognized command received")
               break
      endswitch

endwhile

;Note.  The preceeding loop never exits
exit   ; Just a formality

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;The DoIniWork subroutine is designed to allow a WinBatch script or
;other process running under the user account to "flag" the service
;to perfrom useful work of a kind the user is not privileged to do.
;In this case, perhaps just setting a registry key.

:DoIniWork
   inicmd=StrCat(inipath,"\",inistring)
   ;Check that target executable exists, so need to remove any command line switches
   FileToCheck=StrTrim(ItemExtract("1", inicmd, "/"))
     IniWritePvt("Debug","iniexe",iniexe,inicontrolfile)
     IniWritePvt("Debug","inicmd",inicmd,inicontrolfile)
     IniWritePvt("Debug","inistring",inistring,inicontrolfile)
     IniWritePvt("Debug","inipath",inipath,inicontrolfile)
     IniWritePvt("Debug","FileToCheck",FileToCheck,inicontrolfile)
fex=FileExist(FileToCheck)
   If fex 
   ;target file found, so do something with it
     IniExitCode=RunShell(iniexe,inicmd,inipath,@HIDDEN,@GETEXITCODE)
     IniWritePvt("Control","ExitCode",IniExitCode,inicontrolfile)
   Else
   ;target file not found
     ErrorString=StrCat("Specified file not found: ",FileToCheck)
     IniWritePvt("Control","ServiceError",ErrorString,inicontrolfile)
   EndIf
return



#########################
and here is the debugtrace output:


code=SvcWaitForCmd(5000)
(30170) VALUE INT => 0

else
(30170) END OPERATOR

switch code
(30170) CASE TRUE==>0

inivalue=IniReadPvt("Control","Request",0,inicontrolfile)
(30170) VALUE STRING => "0"

inipath=IniReadPvt("Control","RunPath","",inicontrolfile)
(30170) VALUE STRING => ""

inistring=IniReadPvt("Control","RunString","",inicontrolfile)
(30170) VALUE STRING => ""

iniexe=IniReadPvt("Control","RunEXE","",inicontrolfile)
(30170) VALUE STRING => ""

if inivalue!=0
(30170) END OPERATOR

break
(30170) END OPERATOR

endwhile
(30170) END OPERATOR

while @TRUE
(30170) WHILE==>TRUE

if DoingDebug==@FALSE
(30170) IF DO==>TRUE

code=SvcWaitForCmd(5000)
(35194) VALUE INT => 0

else
(35194) END OPERATOR

switch code
(35194) CASE TRUE==>0

inivalue=IniReadPvt("Control","Request",0,inicontrolfile)
(35194) VALUE STRING => "0"

inipath=IniReadPvt("Control","RunPath","",inicontrolfile)
(35194) VALUE STRING => ""

inistring=IniReadPvt("Control","RunString","",inicontrolfile)
(35194) VALUE STRING => ""

iniexe=IniReadPvt("Control","RunEXE","",inicontrolfile)
(35194) VALUE STRING => ""

if inivalue!=0
(35194) END OPERATOR

break
(35194) END OPERATOR

endwhile
(35194) END OPERATOR

while @TRUE
(35194) WHILE==>TRUE

if DoingDebug==@FALSE
(35194) IF DO==>TRUE

code=SvcWaitForCmd(5000)
(40217) VALUE INT => 0

else
(40217) END OPERATOR

switch code
(40217) CASE TRUE==>0

inivalue=IniReadPvt("Control","Request",0,inicontrolfile)
(40217) VALUE STRING => "128"

inipath=IniReadPvt("Control","RunPath","",inicontrolfile)
(40217) VALUE STRING => "E:\Apps"

inistring=IniReadPvt("Control","RunString","",inicontrolfile)
(40217) VALUE STRING => "dragon.cmd"

iniexe=IniReadPvt("Control","RunEXE","",inicontrolfile)
(40217) VALUE STRING => "C:\Windows\System32\Cmd.exe"

if inivalue!=0
(40217) IF DO==>TRUE

IniWritePvt("Control","Request",0,inicontrolfile)
(40217) VALUE INT => 1

IniWritePvt("Control","RunPath","",inicontrolfile)
(40217) VALUE INT => 1

IniWritePvt("Control","RunString","",inicontrolfile)
(40217) VALUE INT => 1

IniWritePvt("Control","RunEXE","",inicontrolfile)
(40217) VALUE INT => 1

IniWritePvt("Control","ExitCode","",inicontrolfile)
(40217) VALUE INT => 1

IniWritePvt("Control","ServiceError","",inicontrolfile)
(40217) VALUE INT => 1

gosub DoIniWork
(40217) GOSUB

inicmd=StrCat(inipath,"\",inistring)
(40217) VALUE STRING => "E:\Apps\dragon.cmd"

FileToCheck=StrTrim(ItemExtract("1", inicmd, "/"))
(40217) VALUE STRING => "E:\Apps\dragon.cmd"

IniWritePvt("Debug","iniexe",iniexe,inicontrolfile)
(40217) VALUE INT => 1

IniWritePvt("Debug","inicmd",inicmd,inicontrolfile)
(40233) VALUE INT => 1

IniWritePvt("Debug","inistring",inistring,inicontrolfile)
(40233) VALUE INT => 1

IniWritePvt("Debug","inipath",inipath,inicontrolfile)
(40233) VALUE INT => 1

IniWritePvt("Debug","FileToCheck",FileToCheck,inicontrolfile)
(40233) VALUE INT => 1

fex=FileExist(FileToCheck)
(40233) VALUE INT => 1

If fex
(40233) IF DO==>TRUE

################
From the information logged by the program, all the target locations are correct, and the files all exist, but it would appear that the RunShell command is hanging, as no result is returned in the DebugTrace output, and the service cannot be stopped from the command line, and has to be killed using task manager (before any actual RunShell operations are requested, the service starts and stops fine from the command line).
I wondered whether there might be some sort of error dialog being produced which cannot be seen by a user as on Win 7, services cannot interact with the desktop. I also wondered whether there might be a UAC issue, or I'm missing something blatantly obvious that you will be able to point out to me. 
Any suggestions for better error trapping in this sort of non-interactive environment would also be welcomed.
Thanks!
Ed
Using Winbatch since 1995. Excellent tool, awesome support always.

Deana

Thanks for providing so much information about the issue.

Tell me about the application you are attempting to launch. Can it be run without GUI?

I ask because starting in Windows Vista, interactive Windows Services received a security makeover. Services on Vista and newer run in an isolated Session and can not easily communicate with windows in interactive user sessions. If your code above creates any GUI elements, even with the "Allow service to interact with desktop" checked, the script will appear to hang.

Reference:
Windows Services restrictions
http://msdn.microsoft.com/en-us/library/windows/hardware/dn653293(v=vs.85).aspx

One possible workaround ( which can be quite complex):

Interacting with a User from a Service Indirectly. You can use the following techniques to interact with the user from a service on all supported versions of Windows:

  • Display a dialog box in the user's session using the WTSSendMessage function in the Terminal Service Extender.
  • Create a separate hidden GUI WinBatch script and use the RunWithLogon function to run the WinBatch script within the context of the interactive user. Design the GUI WinBatch script to communicate with the service through some method of inter process communication (IPC), for example, named pipes[url http://techsupt.winbatch.com/webcgi/webbatch.exe?techsupt/nftechsupt.web+Tutorials+Named~Pipes.txt[/url]. The service communicates with the GUI WinBatch script to tell it when to display the GUI. The WinBatch script then communicates the results of the user interaction back to the service so that the service can take the appropriate action. Note that IPC can expose your service interfaces over the network unless you use an appropriate access control list (ACL).




Deana F.
Technical Support
Wilson WindowWare Inc.

etippelt

Dragon.cmd contains:  (if I recall correctly)

Net User username password /add
Net LocalGroup Administrators username /add

Running this as an admin appears to work, but now I'm wondering whether I should have used cmd.exe /c and also piped each of the commands to a text file as well.

I'm pretty sure also that this is some aspect of the lack of interaction with the desktop but after spending half a day on this, I ran out of ideas on how to diagnose the problem when everything is running at a different level to that of the user account.
Using Winbatch since 1995. Excellent tool, awesome support always.

Deana

These types of issue can be difficult to debug. Maybe yake a look at the System log in Windows EventViewer (eventvwr.exe) and check out entries with source as 'Service Control Manager'.

I would definitely recommend running cmd.exe with the /c parameter.  This should ensure that the Command shell closes when complete. Have you tried running your .cmd script from your WinBatch script as a logged in user to confirm no prompts?
Deana F.
Technical Support
Wilson WindowWare Inc.