'#-- '# Copyright (c) 2006-2007 Luis Lavena, Multimedia systems '# '# This source code is released under the MIT License. '# See MIT-LICENSE file for details '#++ #include once "ServiceFB.bi" #include once "_internals.bi" namespace fb namespace svc '# I started this as simple, unique service served from one process '# but the idea of share the same process space (and reduce resources use) was good. '# to do that, I needed a references table (similar to service_table, but we will '# hold the ServiceProcess registered by ServiceHost (the multi services host). '# also, I needed a locking mechanism to avoid problems of two calls changing the table '# at the same time. dim shared _svc_references as ServiceProcess ptr ptr dim shared _svc_references_count as integer dim shared _svc_references_lock as any ptr '##################### '# ServiceProcess '# ctor() constructor ServiceProcess() constructor("NewServiceProcess") end constructor '# ctor(name) constructor ServiceProcess(byref new_name as string) _dprint("ServiceProcess(new_name)") '# assign the service name this.name = new_name '# initialize the status structure with this._svcStatus .dwServiceType = SERVICE_WIN32_OWN_PROCESS .dwCurrentState = SERVICE_STOPPED .dwControlsAccepted = (SERVICE_ACCEPT_STOP or SERVICE_ACCEPT_SHUTDOWN) .dwWin32ExitCode = NO_ERROR .dwServiceSpecificExitCode = NO_ERROR .dwCheckPoint = 0 .dwWaitHint = 0 end with '# use a state placeholder this.state = this._svcStatus.dwCurrentState '# disable shared process by default this.shared_process = FALSE '# create the stop event this._svcStopEvent = CreateEvent( 0, FALSE, FALSE, 0 ) _dprint("ServiceProcess(new_name) done") end constructor '# dtor() destructor ServiceProcess() _dprint("ServiceProcess() destructor") '# safe to destroy it. anyway, just checking with this .onInit = 0 .onStart = 0 .onStop = 0 .onPause = 0 .onContinue = 0 ._threadHandle = 0 CloseHandle(._svcStopEvent) end with _dprint("ServiceProcess() destructor done") end destructor '# for single process, here I create the references table and then '# delegate control to _run() which will call the service control dispatcher sub ServiceProcess.Run() _dprint("ServiceProcess.Run()") '# add the unique reference _add_to_references(this) '# delegate control to _run() _run() _dprint("ServiceProcess.Run() done") end sub '# I use this method to simplify changing the service state '# notification to the service manager. '# is needed to set dwControlsAccepted = 0 if state is SERVICE_*_PENDING '# also, StillAlive() call it to set the checkpoint and waithint '# to avoid SCM shut us down. '# is not for the the end-user (*you*) to access it, but implemented in this '# way to reduce needed to pass the right service reference each time sub ServiceProcess.UpdateState(byval state as DWORD, byval checkpoint as integer = 0, byval waithint as integer = 0) _dprint("ServiceProcess.UpdateState()") '# set the state select case state '# if the service is starting or stopping, I must disable the option to accept '# other controls form SCM. case SERVICE_START_PENDING, SERVICE_STOP_PENDING: this._svcStatus.dwControlsAccepted = 0 '# in this case, running or paused, stop and shutdown must be available '# also, we must check here if our service is capable of pause/continue ç '# functionality and allow them (or not). case SERVICE_RUNNING, SERVICE_PAUSED: this._svcStatus.dwControlsAccepted = (SERVICE_ACCEPT_STOP or SERVICE_ACCEPT_SHUTDOWN) '# from start, the service accept stop and shutdown (see ctor(name)). '# configure the accepted controls. '# Pause and Continue only will be enabled if you setup onPause and onContinue if not (this.onPause = 0) and _ not (this.onContinue = 0) then this._svcStatus.dwControlsAccepted or= SERVICE_ACCEPT_PAUSE_CONTINUE end if end select '# set the structure status '# also the property this._svcStatus.dwCurrentState = state this.state = state '# set checkpoint and waithint this._svcStatus.dwCheckPoint = checkpoint this._svcStatus.dwWaitHint = waithint '# call the API '# only we will call is _svcHandle is valid '# this will allow use of UpdateState (and StillAlive) from console if not (this._svcHandle = 0) then _dprint("SetServiceStatus() API") SetServiceStatus(this._svcHandle, @this._svcStatus) end if _dprint("ServiceProcess.UpdateState() done") end sub '# use StillAlive() method when performing lengthly tasks during onInit or onStop '# (if they take too much time). '# by default we set a wait hint gap of 10 seconds, but you could specify how many '# you could specify how many seconds more will require your *work* sub ServiceProcess.StillAlive(byval waithint as integer = 10) dim as integer checkpoint _dprint("ServiceProcess.StillAlive()") '# start or stop pending? if (this._svcStatus.dwCurrentState = SERVICE_START_PENDING) or _ (this._svcStatus.dwCurrentState = SERVICE_STOP_PENDING) then with this checkpoint = this._svcStatus.dwCheckPoint checkpoint += 1 .UpdateState(._svcStatus.dwCurrentState, checkpoint, (waithint * 1000)) end with end if _dprint("ServiceProcess.StillAlive() done") end sub '# call_onStart() is a wrapper around the new limitation of threadcreate '# sub used as pointers in threadcreate must conform the signature sub ServiceProcess.call_onStart(byval any_service as any ptr) var service = cast(ServiceProcess ptr, any_service) service->onStart(*service) end sub '##################### '# ServiceHost '# ctor() '# currently isn't needed, why I defined it? constructor ServiceHost() _dprint("ServiceHost()") _dprint("ServiceHost() done") end constructor '# dtor() '# currently isn't needed, why I defined it? destructor ServiceHost() _dprint("ServiceHost() destructor") _dprint("ServiceHost() destructor done") end destructor '# using Add() will register an already initialized service into the references '# table, which will be used later to launch and control the different services '# we should be careful when handling references, so for that reference_lock is '# provided ;-) sub ServiceHost.Add(byref service as ServiceProcess) _dprint("ServiceHost.Add()") '# add the service reference to the references table '# get the new count as result, so '# increment the local counter this.count = _add_to_references(service) _dprint("ServiceHost.Add() done") end sub '# ServiceHost.Run() is just a placeholder, it delegates control to _run() '# pretty simple, but still must be present to simplify user interaction. sub ServiceHost.Run() _dprint("ServiceHost.Run()") '# the longest, hard coded function in the world! '# just kidding _run() _dprint("ServiceHost.Run() done") end sub '# the purpose of this sub is provide a generic service creation and running '# this is be called from exisitng ServiceProcess and ServiceHost. '# this construct the SERVICE_TABLE_ENTRY based on the the references table, '# which will be sent to StartServiceCtrlDispatcher() private sub _run() dim ServiceTable(_svc_references_count) as SERVICE_TABLE_ENTRY dim idx as integer _dprint("_run()") _dprint("creating service table for " + str(_svc_references_count) + " services") for idx = 0 to (_svc_references_count - 1) '# we take the service name from the references and set as ServiceMain the same '# _main() routine for all the services ServiceTable(idx) = type(strptr(_svc_references[idx]->name), @_main) _dprint(str(idx) + ": " + _svc_references[idx]->name) next idx '# last member of the table must be null ServiceTable(_svc_references_count) = type(0, 0) _dprint("service table created") '# start the dispatcher _dprint("start service dispatcher") StartServiceCtrlDispatcher( @ServiceTable(0) ) _dprint("_run() done") end sub '# this sub is fired by StartServiceCtrlDispatcher in another thread. '# because it is a global _main for all the services in the table, looking up '# in the references for the right service is needed prior registering its '# control handler. private sub _main(byval argc as DWORD, byval argv as LPSTR ptr) dim success as integer dim service as ServiceProcess ptr dim run_mode as string dim service_name as string dim commandline as string dim param_line as string dim temp as string _dprint("_main()") '# debug dump of argc and argv dim idx as integer = 0 for idx = 0 to (argc - 1) _dprint(str(idx) + ": " + *argv[idx]) next idx '# retrieve all the information (mode, service name and command line _build_commandline(run_mode, service_name, commandline) service = _find_in_references(service_name) '# build parameter line (passed from SCM) if (argc > 1) then param_line = "" for idx = 1 to (argc - 1) temp = *argv[idx] if (instr(temp, chr(32)) > 0) then param_line += """" + temp + """" else param_line += temp end if param_line += " " next idx end if '# parameters passed using SCM have priority over ImagePath ones if not (len(param_line) = 0) then commandline = param_line end if '# a philosofical question: to run or not to run? if not (service = 0) then _dprint("got a valid service reference") _dprint("real service name: " + service->name) '# pass to the service the commandline _dprint("passing service commandline: " + commandline) service->commandline = commandline '# ok, its a service!, its alive! '# register his ControlHandlerEx _dprint("register control handler ex") service->_svcHandle = RegisterServiceCtrlHandlerEx(strptr(service_name), @_control_ex, cast(LPVOID, service)) '# check if evething is done right if not (service->_svcHandle = 0) then '# now, we are a single service or a bunch, like the bradys? if (_svc_references_count > 1) then '# determine if we share or not the process if (service->shared_process = FALSE) then service->_svcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS else '# this mean we will be sharing... hope neighbors don't crash the house! service->_svcStatus.dwServiceType = SERVICE_WIN32_SHARE_PROCESS end if else '# ok, we have a full house (ehem, process) for us only! service->_svcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS end if '# START_PENDING _dprint("service start pending") service->UpdateState(SERVICE_START_PENDING) '# now delegate to the long running initialization if it exist. if not (service->onInit = 0) then _dprint("pass control to lengthly initialization") success = service->onInit(*service) else '# if no onInit was defined (maybe you don't need it?) '# we should simulate it was successful to proceed success = (-1) end if _dprint("onInit result: " + str(success)) '# check if everything is ok '# if onInit showed problems, 0 was returned and service must not continue if not (success = 0) then '# SERVICE_RUNNING '# we must launch the onStart as thread, but first setting state as running service->UpdateState(SERVICE_RUNNING) if not (service->onStart = 0) then _dprint("dispatch onStart() as new thread") service->_threadHandle = threadcreate(@ServiceProcess.call_onStart, service) '# my guess? was a hit! end if '# now that we are out of onStart thread, check if actually hit the stop sign _dprint("waiting for stop signal") do '# do nothing ... '# but not too often! loop while (WaitForSingleObject(service->_svcStopEvent, 100) = WAIT_TIMEOUT) '# now, wait for the thread (anyway, I hope it will be checking this.state, right?) '# we should do this, or actualy jump and wait for StopEvent? _dprint("waiting for onStart() thread to finish") threadwait(service->_threadHandle) end if '# if we reach here, that means the service is not running, and the onStop was performed '# so no more chat, stop it one and for all! '# set SERVICE_STOPPED (just checking) _dprint("service stopped") service->UpdateState(SERVICE_STOPPED) end if '# ok, we are done! end if _dprint("_main() done") end sub '# this sub is used by _main when registering the ControlHandler for this service '# (as callback from service manager). '# we process each control codes and perform the actions using the pseudo-events (callbacks) '# also we use lpContext to get the right reference when _main registered the control handler. private function _control_ex(byval dwControl as DWORD, byval dwEventType as DWORD, byval lpEventData as LPVOID, byval lpContext as LPVOID) as DWORD dim result as DWORD dim service as ServiceProcess ptr _dprint("_control_ex()") '# we get a reference form the context service = cast(ServiceProcess ptr, lpContext) '# show if the service reference is valid? _dprint("service name: " + service->name) select case dwControl case SERVICE_CONTROL_INTERROGATE: '# we are running, so what we should do here? _dprint("interrogation signal received") '# in case we get a interrogation, we always should answer this way. result = NO_ERROR case SERVICE_CONTROL_SHUTDOWN, SERVICE_CONTROL_STOP: _dprint("stop signal received") '# ok, service manager requested us to stop. '# we must call onStop if was defined. service->UpdateState(SERVICE_STOP_PENDING) if not (service->onStop = 0) then _dprint("pass control to onStop()") service->onStop(*service) end if '# now signal the stop event so _main could take care of the rest. _dprint("signal stop event") SetEvent(service->_svcStopEvent) case SERVICE_CONTROL_PAUSE: _dprint("pause signal received") '# we must check if we could answer to the request. if not (service->onPause = 0) and _ not (service->onContinue = 0) then '# just to be sure if not (service->onPause = 0) then service->UpdateState(SERVICE_PAUSE_PENDING) _dprint("pass control to onPause()") service->onPause(*service) service->UpdateState(SERVICE_PAUSED) _dprint("service paused") end if result = NO_ERROR else '# ok, our service didn't support pause or continue '# tell the service manager about that! result = ERROR_CALL_NOT_IMPLEMENTED end if case SERVICE_CONTROL_CONTINUE: _dprint("continue signal received") '# we should resume from a paused state '# we must check if we could answer to the request. if not (service->onPause = 0) and _ not (service->onContinue = 0) then '# just to be sure if not (service->onPause = 0) then service->UpdateState(SERVICE_CONTINUE_PENDING) _dprint("pass control to onContinue()") service->onContinue(*service) service->UpdateState(SERVICE_RUNNING) _dprint("service running") end if result = NO_ERROR else '# ok, our service didn't support pause or continue '# tell the service manager about that! result = ERROR_CALL_NOT_IMPLEMENTED end if case else: result = NO_ERROR end select _dprint("_control_ex() done") return result end function '# add_to_references is a helper used to reduce code duplication (DRY). '# here is used a lock around _svc_references to avoid two threads try change the '# reference count (just in case). function _add_to_references(byref service as ServiceProcess) as integer _dprint("_add_to_references()") '# get a lock before even think touch references! mutexlock(_svc_references_lock) '# now, reallocate space _svc_references_count += 1 _svc_references = reallocate(_svc_references, sizeof(ServiceProcess ptr) * _svc_references_count) '# put the reference of this service into the table _svc_references[(_svc_references_count - 1)] = @service '# ok, done, unlock our weapons! ;-) mutexunlock(_svc_references_lock) _dprint("_add_to_references() done") '# return the new references count return _svc_references_count end function '# find_in_references is used by _main to lookup for the specified service in '# references table. function _find_in_references(byref service_name as string) as ServiceProcess ptr dim result as ServiceProcess ptr dim item as ServiceProcess ptr dim idx as integer _dprint("_find_in_references()") '# we start with a pesimistic idea ;-) result = 0 for idx = 0 to (_svc_references_count - 1) '# hold a reference to the item item = _svc_references[idx] '# compare if we have a match if (service_name = item->name) then result = item exit for end if next idx _dprint("_find_in_references() done") '# return the found (or not) reference return result end function '# namespace constructor '# first we must create the mutex to be used with references private sub _initialize() constructor _dprint("_initialize() constructor") '# we do this in case was already defined... don't know the situation, '# just to be sure if (_svc_references_lock = 0) then _svc_references_lock = mutexcreate() '# also initialize our count :-) _svc_references_count = 0 end if _dprint("_initialize() constructor done") end sub '# namespace destructor private sub _terminate() destructor _dprint("_terminate() destructor") '# to avoid removing everything, we must lock to the references mutexlock(_svc_references_lock) '# destroy our refernces allocated memory! deallocate(_svc_references) '# unlock the mutex and destroy it too. mutexunlock(_svc_references_lock) mutexdestroy(_svc_references_lock) _dprint("_terminate() destructor done") end sub '# command line builder (helper) '# this is used to gather information about: '# mode (if present) '# valid service name (after lookup in the table) '# command line to be passed to service sub _build_commandline(byref mode as string, byref service_name as string, byref commandline as string) dim result_mode as string dim result_name as string dim result_cmdline as string dim service as ServiceProcess ptr dim idx as integer dim temp as string idx = 1 '# first, determine if mode is pressent in commandline, must me command(1) temp = lcase(command(idx)) if (temp = "console") or _ (temp = "manage") or _ (temp = "service") then result_mode = temp idx += 1 end if '# now, check if service name is present temp = command(idx) '# its present? if (len(temp) > 0) then '# lookup in references table service = _find_in_references(temp) if not (service = 0) then '# was found, so must be valid result_name = temp '# adjust start index for cmdline idx += 1 end if end if '# is service valid? '# its really needed? if (service = 0) then if (_svc_references_count = 1) then '# no, get the first one service = _svc_references[0] result_name = service->name '# adjust start index for cmdline else '# this is needed! result_name = "" end if end if result_cmdline = "" temp = command(idx) do while (len(temp) > 0) if (instr(temp, chr(32)) > 0) then '# properly quote parameters with spaces result_cmdline += """" + temp + """" else result_cmdline += temp end if result_cmdline += " " idx += 1 temp = command(idx) loop '# now, return the results mode = result_mode service_name = result_name commandline = result_cmdline end sub '# ### DEBUG ### '# just for debuging purposes '# (will be removed in the future when Loggers get implemented) #ifdef SERVICEFB_DEBUG_LOG sub _dprint(byref message as string) dim handle as integer handle = freefile open EXEPATH + "\servicefb.log" for append as #handle print #handle, message close #handle end sub #endif end namespace '# fb.svc end namespace '# fb