thin_service 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ '#--
2
+ '# Copyright (c) 2006-2007 Luis Lavena, Multimedia systems
3
+ '#
4
+ '# This source code is released under the MIT License.
5
+ '# See MIT-LICENSE file for details
6
+ '#++
7
+
8
+ #if __FB_VERSION__ < "0.17"
9
+ #error ServiceFB is designed to compile with FreeBASIC version "0.17"
10
+ #else
11
+
12
+ #ifndef __FB_WIN32__
13
+ #error Platform unsupported. Compiling ServiceFB requires Windows platform.
14
+ #else
15
+
16
+ #ifndef __ServiceFB_bi__
17
+ #define __ServiceFB_bi__
18
+
19
+ #include once "windows.bi"
20
+ #inclib "advapi32"
21
+
22
+ namespace fb
23
+ namespace svc '# fb.svc
24
+ #ifdef SERVICEFB_DEBUG_LOG
25
+ '# debug print
26
+ declare sub _dprint(byref as string)
27
+ #else
28
+ #define _dprint(message)
29
+ #endif
30
+
31
+ '# service states used by end user with 'state' property
32
+ enum ServiceStateEnum
33
+ Running = SERVICE_RUNNING
34
+ Paused = SERVICE_PAUSED
35
+ Stopped = SERVICE_STOPPED
36
+ end enum
37
+
38
+
39
+ '# ServiceProcess type (object)
40
+ '# use this to create new services and reference the on*() methods to perform the related
41
+ '# tasks.
42
+ type ServiceProcess
43
+ '# ctor/dtor
44
+ declare constructor()
45
+ declare constructor(byref as string)
46
+ declare destructor()
47
+
48
+ '# methods (public)
49
+ declare sub Run()
50
+ declare sub StillAlive(byval as integer = 10)
51
+
52
+ '# helper methods (private)
53
+ declare sub UpdateState(byval as DWORD, byval as integer = 0, byval as integer = 0)
54
+
55
+ '# pseudo-events
56
+ '# for onInit you should return FALSE (0) in case you want to abort
57
+ '# service initialization.
58
+ '# If everything was ok, then return TRUE (-1)
59
+ onInit as function(byref as ServiceProcess) as integer
60
+ onStart as sub(byref as ServiceProcess)
61
+ onStop as sub(byref as ServiceProcess)
62
+ onPause as sub(byref as ServiceProcess)
63
+ onContinue as sub(byref as ServiceProcess)
64
+
65
+ '# call_* are used to avoid the warning arround ThreadCreate
66
+ declare static sub call_onStart(byval as any ptr)
67
+
68
+ '# properties (public)
69
+ name as string
70
+ description as string
71
+ state as ServiceStateEnum
72
+ commandline as string '# TODO
73
+ shared_process as integer
74
+
75
+ '# properties (private)
76
+ _svcStatus as SERVICE_STATUS
77
+ _svcHandle as SERVICE_STATUS_HANDLE
78
+ _svcStopEvent as HANDLE
79
+ _threadHandle as any ptr
80
+ end type
81
+
82
+
83
+ '# ServiceHost type (object)
84
+ '# use this, beside ServiceProcess, to manage the registration and running of
85
+ '# several services sharing the same process.
86
+ '# NOTE: ServiceHost.Run() and ServiceProcess.Run() are mutually exclusive, that
87
+ '# means don't mix single service with multiple service in the same program!
88
+ type ServiceHost
89
+ '# ctor/dtor()
90
+ declare constructor()
91
+ declare destructor()
92
+
93
+ '# methods (public)
94
+ declare sub Add(byref as ServiceProcess)
95
+ declare sub Run()
96
+
97
+ '# properties (public)
98
+ count as integer
99
+ end type
100
+ end namespace '# fb.svc
101
+ end namespace '# fb
102
+
103
+ #ifdef SERVICEFB_INCLUDE_UTILS
104
+ #include once "ServiceFB_Utils.bi"
105
+ #endif
106
+
107
+ #endif '# __ServiceFB_bi__
108
+ #endif '# __FB_WIN32__
109
+ #endif '# __FB_VERSION__
@@ -0,0 +1,495 @@
1
+ '#--
2
+ '# Copyright (c) 2006-2007 Luis Lavena, Multimedia systems
3
+ '#
4
+ '# This source code is released under the MIT License.
5
+ '# See MIT-LICENSE file for details
6
+ '#++
7
+
8
+ #include once "ServiceFB.bi"
9
+ #include once "_internals.bi"
10
+ #include once "ServiceFB_Utils.bi"
11
+ #include once "_utils_internals.bi"
12
+
13
+ namespace fb
14
+ namespace svc
15
+ namespace utils '# fb.svc.utils
16
+ '# private (internals) for ServiceProcess.Console()
17
+ dim shared _svc_stop_signal as any ptr
18
+ dim shared _svc_stop_mutex as any ptr
19
+ dim shared _svc_stopped as BOOL
20
+ dim shared _svc_in_console as ServiceProcess ptr
21
+ dim shared _svc_in_console_stop_flag as BOOL
22
+
23
+ '#####################
24
+ '# ServiceController
25
+ '# ctor()
26
+ constructor ServiceController()
27
+ with this
28
+ .product = "My Product"
29
+ .version = "v0.1"
30
+ .copyright = "my copyright goes here."
31
+ end with
32
+ end constructor
33
+
34
+
35
+ '# ctor(product)
36
+ constructor ServiceController(byref new_product as string)
37
+ this.product = new_product
38
+ end constructor
39
+
40
+
41
+ '# ctor(product, version)
42
+ constructor ServiceController(byref new_product as string, byref new_version as string)
43
+ constructor(new_product)
44
+ this.version = new_version
45
+ end constructor
46
+
47
+
48
+ '# ctor(product, version, copyright)
49
+ constructor ServiceController(byref new_product as string, byref new_version as string, byref new_copyright as string)
50
+ constructor(new_product, new_version)
51
+ this.copyright = new_copyright
52
+ end constructor
53
+
54
+
55
+ '# dtor()
56
+ destructor ServiceController()
57
+ end destructor
58
+
59
+
60
+ '# Banner() will display in the console, information regarding your program
61
+ '# using this formatting:
62
+ '# 'Product', 'Version'
63
+ '# 'Copyright'
64
+ sub ServiceController.Banner()
65
+ '# display Product and Version
66
+ print this.product; ", "; this.version
67
+ print this.copyright
68
+ print ""
69
+ '# leave a empty line between banner (header) and other info
70
+ end sub
71
+
72
+
73
+ '# RunMode() provide a simple way to get (*you*) from where this process was started
74
+ '# and do the corresponding action.
75
+ function ServiceController.RunMode() as ServiceRunMode
76
+ dim result as ServiceRunMode
77
+ dim currPID as DWORD
78
+ dim parent_pid as uinteger
79
+ dim parent_name as string
80
+ dim start_mode as string
81
+
82
+ _dprint("ServiceController.RunMode()")
83
+
84
+ '# get this process PID
85
+ currPID = GetCurrentProcessId()
86
+ _dprint("CurrentPID: " + str(currPID))
87
+
88
+ '# get the parent PID
89
+ parent_pid = _parent_pid(currPID)
90
+ _dprint("ParentPID: " + str(parent_pid))
91
+
92
+ '# now the the name
93
+ parent_name = _process_name(parent_pid)
94
+ if (parent_name = "<unknown>") then
95
+ parent_name = _process_name_dyn_psapi(parent_pid)
96
+ end if
97
+ _dprint("Parent Name: " + parent_name)
98
+
99
+ '# this process started as service?
100
+ '# that means his parent is services.exe
101
+ if (parent_name = "services.exe") then
102
+ result = RunAsService
103
+ else
104
+ '# ok, it didn't start as service, analyze command line then
105
+ start_mode = lcase(trim(command(1)))
106
+ if (start_mode = "manage") then
107
+ '# start ServiceController.Manage()
108
+ result = RunAsManager
109
+ elseif (start_mode = "console") then
110
+ '# start ServiceController.Console()
111
+ result = RunAsConsole
112
+ elseif (start_mode = "service") then
113
+ result = RunAsService
114
+ else
115
+ '# ok, the first paramenter in the commandline didn't work,
116
+ '# report back so we could send the banner!
117
+ result = RunAsUnknown
118
+ end if
119
+ end if
120
+
121
+ _dprint("ServiceController.RunMode() done")
122
+ return result
123
+ end function
124
+
125
+
126
+ '# Manage will offer the user (end-user) option in the commandline to
127
+ '# install, remove, start, stop and query the status of the installed service
128
+ '# use Manage() when you code a multi-services (ServiceHost) based programs
129
+ '# for single services, use Manage(service)
130
+ sub ServiceController.Manage()
131
+ end sub
132
+
133
+
134
+ '# this is used when you want management capabilities for your service
135
+ '# use this for single services, or call Manage() for multi services
136
+ sub ServiceController.Manage(byref service as ServiceProcess)
137
+ end sub
138
+
139
+
140
+ '# this offer the user a way to test/debug your service or run it like a normal
141
+ '# program, from the command line
142
+ '# will let you SHUTDOWN the service using CTRL+C
143
+ '# use this for multi-services (ServiceHost) based programs
144
+ sub ServiceController.Console()
145
+ dim working_thread as any ptr
146
+ dim run_mode as string
147
+ dim service_name as string
148
+ dim service as ServiceProcess ptr
149
+ dim commandline as string
150
+ dim success as integer
151
+
152
+ _dprint("ServiceController.Console()")
153
+
154
+ '# show the controller banner
155
+ this.Banner()
156
+
157
+ '# determine how many service exist in references
158
+ if (_svc_references_count > 0) then
159
+ _build_commandline(run_mode, service_name, commandline)
160
+ service = _find_in_references(service_name)
161
+
162
+ if (service = 0) then
163
+ '# no valid service reference, list available services
164
+ _list_references()
165
+ else
166
+ '# build the command line, excluding 'console' and service_name
167
+ service->commandline = commandline
168
+
169
+ '# got a service reference
170
+ '# also, set the global handler that will be used by _control_handler
171
+ _svc_in_console = service
172
+
173
+ '# create the signal used to stop the service thread.
174
+ _svc_stop_signal = condcreate()
175
+ _svc_stop_mutex = mutexcreate()
176
+
177
+ '# register the Console Handler
178
+ SetConsoleCtrlHandler(@_console_handler, TRUE)
179
+
180
+ print "Starting service '"; service_name; "' in console mode, please wait..."
181
+
182
+ '# onInit should be started inline,
183
+ '# and its result validated!
184
+ if not (service->onInit = 0) then
185
+ success = service->onInit(*service)
186
+ end if
187
+
188
+ '# only continue if success
189
+ if not (success = 0) then
190
+ '# now set service.state to running
191
+ service->state = Running
192
+
193
+ '# now, fire the main loop (onStart)
194
+ if not (service->onStart = 0) then
195
+ '# create the thread
196
+ working_thread = threadcreate(@ServiceProcess.call_onStart, service)
197
+ if (working_thread = 0) then
198
+ print "Failed to create working thread."
199
+ end if
200
+ end if
201
+
202
+ print "Service is in running state."
203
+ print "Press Ctrl-C to stop it."
204
+
205
+ '# now that onStart is running, must monitor the stop_signal
206
+ '# in case it arrives, the service state must change to exit the
207
+ '# working thread.
208
+ mutexlock(_svc_stop_mutex)
209
+ do while (_svc_stopped = FALSE)
210
+ condwait(_svc_stop_signal, _svc_stop_mutex)
211
+ loop
212
+ mutexunlock(_svc_stop_mutex)
213
+
214
+ print "Stop signal received, stopping..."
215
+
216
+ '# received the signal, so set state = Stopped
217
+ service->state = Stopped
218
+
219
+ print "Waiting for onStart() to exit..."
220
+
221
+ '# now wait for the thread to terminate
222
+ if not (working_thread = 0) then
223
+ threadwait(working_thread)
224
+ end if
225
+
226
+ else
227
+ print "Error starting the service, onInit() failed."
228
+ end if
229
+
230
+ print "Service stopped, doing cleanup."
231
+
232
+ '# remove the console handler
233
+ SetConsoleCtrlHandler(@_console_handler, FALSE)
234
+
235
+ '# now that service was stopped, destroy the references.
236
+ conddestroy(_svc_stop_signal)
237
+ mutexdestroy(_svc_stop_mutex)
238
+ print "Done."
239
+ end if
240
+ else
241
+ print "ERROR: No services could be served by this program. Exiting."
242
+ end if
243
+
244
+ _dprint("ServiceController.Console() done")
245
+ end sub
246
+
247
+
248
+ '# this offer the user a way to test/debug your service or run it like a normal
249
+ '# program, from the command line
250
+ '# will let you SHUTDOWN the service using CTRL+C
251
+ '# use this for single-services
252
+ sub ServiceController.Console(byref service as ServiceProcess)
253
+
254
+ _dprint("ServiceController.RunMode(service)")
255
+
256
+ '# register the service in the references table
257
+ _add_to_references(service)
258
+
259
+ _dprint("delegate to Console()")
260
+ '# now delegate control to Console()
261
+ this.Console()
262
+
263
+ _dprint("ServiceController.Console(service) done")
264
+ end sub
265
+
266
+
267
+ '# console_handler is used to get feedback form keyboard and allow
268
+ '# shutdown of service using Ctrl+C / Ctrl+Break from keyboard
269
+ function _console_handler(byval dwCtrlType as DWORD) as BOOL
270
+ dim result as BOOL
271
+ dim service as ServiceProcess ptr
272
+
273
+ _dprint("_console_handler()")
274
+
275
+ '# get the reference from svc_in_console
276
+ service = _svc_in_console
277
+
278
+ '# we default processing of the message to false
279
+ result = FALSE
280
+
281
+ '# avoid recursion problems
282
+ if (_svc_in_console_stop_flag = FALSE) then
283
+ _dprint("no previous signaled, process event")
284
+ '# all the CtrlType events listed will raise the onStop
285
+ '# of the service
286
+ '# here also will be raised the _svc_stop_signal
287
+ select case dwCtrlType
288
+ case CTRL_C_EVENT, CTRL_CLOSE_EVENT, CTRL_BREAK_EVENT, CTRL_LOGOFF_EVENT, CTRL_SHUTDOWN_EVENT:
289
+ _dprint("got supported CTRL_*_EVENT")
290
+ '# avoid recursion problems
291
+ _svc_in_console_stop_flag = TRUE
292
+ _dprint("set signaled to TRUE")
293
+
294
+ '# the service defined onStop?
295
+ if not (service->onStop = 0) then
296
+ _dprint("pass control to onStop()")
297
+ service->onStop(*service)
298
+ end if
299
+
300
+ '# now fire the signal
301
+ _dprint("fire stop signal")
302
+ mutexlock(_svc_stop_mutex)
303
+ condsignal(_svc_stop_signal)
304
+ result = TRUE
305
+ _svc_in_console_stop_flag = FALSE
306
+ _svc_stopped = TRUE
307
+ mutexunlock(_svc_stop_mutex)
308
+
309
+ case else:
310
+ _dprint("unsupported CTRL EVENT")
311
+ result = FALSE
312
+ end select
313
+ else
314
+ _dprint("already running onStop(), do not pass the message to other message handlers!")
315
+ result = TRUE
316
+ end if
317
+
318
+ _dprint("_console_handler() done")
319
+ return result
320
+ end function
321
+
322
+
323
+ '# helper private subs used to list the services and their descriptions
324
+ '# in _svc_references
325
+ private sub _list_references()
326
+ dim item as ServiceProcess ptr
327
+ dim idx as integer
328
+
329
+ print "Available services in this program:"
330
+
331
+ for idx = 0 to (_svc_references_count - 1)
332
+ item = _svc_references[idx]
333
+
334
+ print space(2);
335
+ print trim(item->name), , trim(item->description)
336
+ next idx
337
+
338
+ end sub
339
+
340
+
341
+ '# TODO: SimpleLogger
342
+ '# TODO: EventLogger
343
+
344
+
345
+ '#####################
346
+ '# private (internals)
347
+ '# _parent_pid is used to retrieve, based on the PID you passed by, the one of the parent
348
+ '# that launched that process.
349
+ '# on fail, it will return 0
350
+ '# Thanks to MichaelW (FreeBASIC forums) for his help about this.
351
+ private function _parent_pid(byval PID as uinteger) as uinteger
352
+ dim as uinteger result
353
+ dim as HANDLE hProcessSnap
354
+ dim as PROCESSENTRY32 pe32
355
+
356
+ '# initialize result, 0 = fail, other number, ParentPID
357
+ result = 0
358
+
359
+ hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
360
+ if not (hProcessSnap = INVALID_HANDLE_VALUE) then
361
+ pe32.dwSize = sizeof(PROCESSENTRY32)
362
+ if (Process32First(hProcessSnap, @pe32) = TRUE) then
363
+ do
364
+ if (pe32.th32ProcessID = PID) then
365
+ result = pe32.th32ParentProcessID
366
+ exit do
367
+ end if
368
+ loop while not (Process32Next(hProcessSnap, @pe32) = 0)
369
+ end if
370
+ end if
371
+
372
+ CloseHandle(hProcessSnap)
373
+ return result
374
+ end function
375
+
376
+
377
+ '# _process_name is used to retrieve the name (ImageName, BaseModule, whatever) of the PID you
378
+ '# pass to it. if no module name was found, it should return <unknown>
379
+ private function _process_name(byval PID as uinteger) as string
380
+ dim result as string
381
+ dim hProcess as HANDLE
382
+ dim hMod as HMODULE
383
+ dim cbNeeded as DWORD
384
+
385
+ '# assign "<unknown>" to process name, allocate MAX_PATH (260 bytes)
386
+ result = "<unknown>"
387
+ result += space(MAX_PATH - len(result))
388
+
389
+ '# get a handle to the Process
390
+ hProcess = OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, FALSE, PID)
391
+
392
+ '# if valid, get the process name
393
+ if not (hProcess = NULL) then
394
+ '# success getting Process modules
395
+ if not (EnumProcessModules(hProcess, @hMod, sizeof(hMod), @cbNeeded) = 0) then
396
+ result = space(cbNeeded)
397
+ GetModuleBaseName(hProcess, hMod, strptr(result), len(result))
398
+ end if
399
+ end if
400
+
401
+ CloseHandle(hProcess)
402
+
403
+ '# return a trimmed result
404
+ result = trim(result)
405
+ return result
406
+ end function
407
+
408
+ '# _process_name_dyn_psapi is a workaround for some issues with x64 versions of Windows.
409
+ '# by default, 32bits process can't query information from 64bits modules.
410
+ private function _process_name_dyn_psapi(byval PID as uinteger) as string
411
+ dim result as string
412
+ dim chop as uinteger
413
+ dim zresult as zstring * MAX_PATH
414
+ dim hLib as any ptr
415
+ dim hProcess as HANDLE
416
+ dim cbNeeded as DWORD
417
+ dim GetProcessImageFileName as function (byval as HANDLE, byval as LPTSTR, byval as DWORD) as DWORD
418
+
419
+ '# assign "<unknown>" to process name, allocate MAX_PATH (260 bytes)
420
+ zresult = "<unknown>" + chr(0)
421
+
422
+ '# get dynlib
423
+ hLib = dylibload("psapi.dll")
424
+ if not (hlib = 0) then
425
+ GetProcessImageFileName = dylibsymbol(hlib, "GetProcessImageFileNameA")
426
+ if not (GetProcessImageFileName = 0) then
427
+ '# get a handle to the Process
428
+ hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, PID)
429
+
430
+ '# if valid, get the process name
431
+ if not (hProcess = NULL) then
432
+ cbNeeded = sizeof(zresult)
433
+ if (GetProcessImageFileName(hProcess, @zresult, cbNeeded) = 0) then
434
+ _dprint("Error with GetProcessImageFileName")
435
+ _dprint("GetLastError: " + str(GetLastError()) + _show_error())
436
+ else
437
+ result = zresult
438
+ chop = InStrRev(0, result, "\")
439
+ if (chop > 0) then
440
+ result = mid(result, chop + 1, (len(result) - chop))
441
+ end if
442
+ end if
443
+ else
444
+ _dprint("Error with OpenProcess")
445
+ _dprint("GetLastError: " + str(GetLastError()) + _show_error())
446
+ end if
447
+
448
+ CloseHandle(hProcess)
449
+ else
450
+ _dprint("Unable to get a reference to dynamic symbol GetProcessImageFileNameA.")
451
+ end if
452
+ else
453
+ _dprint("Unable to dynamic load psapi.dll")
454
+ end if
455
+
456
+ '# return a trimmed result
457
+ 'result = trim(result)
458
+ return result
459
+ end function
460
+
461
+ private function _show_error() as string
462
+ dim buffer as string * 1024
463
+ dim p as integer
464
+
465
+ FormatMessage( FORMAT_MESSAGE_FROM_SYSTEM,_
466
+ 0,_
467
+ GetLastError(),_
468
+ 0,_
469
+ strptr(buffer),_
470
+ 1024,_
471
+ 0 )
472
+ buffer = rtrim(buffer)
473
+ p = instr(buffer, chr(13))
474
+ if p then buffer = left(buffer, p - 1)
475
+
476
+ return buffer
477
+ end function
478
+
479
+ private function InStrRev(byval start as uinteger = 0, byref src as string, byref search as string) as uinteger
480
+ dim lensearch as uinteger = len(search)
481
+ dim as uinteger b, a = 0, exit_loop = 0
482
+
483
+ do
484
+ b = a
485
+ a += 1
486
+ a = instr(a, src, search)
487
+ if start >= lensearch then if a + lensearch > start then exit_loop = 1
488
+ loop while (a > 0) and (exit_loop = 0)
489
+
490
+ return b
491
+ end function
492
+
493
+ end namespace '# fb.svc.utils
494
+ end namespace '# fb.svc
495
+ end namespace '# fb