1 # Runs the build-bot as a Windows service.
3 # * Install and configure buildbot as per normal (ie, running
4 # 'setup.py install' from the source directory).
6 # * Configure any number of build-bot directories (slaves or masters), as
7 # per the buildbot instructions. Test these directories normally by
8 # using the (possibly modified) "buildbot.bat" file and ensure everything
9 # is working as expected.
11 # * Install the buildbot service. Execute the command:
12 # % python buildbot_service.py
13 # To see installation options. You probably want to specify:
14 # + --username and --password options to specify the user to run the
15 # + --startup auto to have the service start at boot time.
18 # % python buildbot_service.py --user mark --password secret \
19 # --startup auto install
20 # Alternatively, you could execute:
21 # % python buildbot_service.py install
22 # to install the service with default options, then use Control Panel
25 # * Start the service specifying the name of all buildbot directories as
26 # service args. This can be done one of 2 ways:
27 # - Execute the command:
28 # % python buildbot_service.py start "dir_name1" "dir_name2"
30 # - Start Control Panel->Administrative Tools->Services
31 # - Locate the previously installed buildbot service.
32 # - Open the "properties" for the service.
33 # - Enter the directory names into the "Start Parameters" textbox. The
34 # directory names must be fully qualified, and surrounded in quotes if
35 # they include spaces.
36 # - Press the "Start"button.
37 # Note that the service will automatically use the previously specified
38 # directories if no arguments are specified. This means the directories
39 # need only be specified when the directories to use have changed (and
40 # therefore also the first time buildbot is configured)
42 # * The service should now be running. You should check the Windows
43 # event log. If all goes well, you should see some information messages
44 # telling you the buildbot has successfully started.
46 # * If you change the buildbot configuration, you must restart the service.
47 # There is currently no way to ask a running buildbot to reload the
48 # config. You can restart by executing:
49 # % python buildbot_service.py restart
52 # * Check the Windows event log for any errors.
53 # * Check the "twistd.log" file in your buildbot directories - once each
54 # bot has been started it just writes to this log as normal.
56 # % python buildbot_service.py debug
57 # This will execute the buildbot service in "debug" mode, and allow you to
58 # see all messages etc generated. If the service works in debug mode but
59 # not as a real service, the error probably relates to the environment or
60 # permissions of the user configured to run the service (debug mode runs as
61 # the currently logged in user, not the service user)
62 # * Ensure you have the latest pywin32 build available, at least version 206.
64 # Written by Mark Hammond, 2006.
80 import win32serviceutil
83 # Are we running in a py2exe environment?
84 is_frozen
= hasattr(sys
, "frozen")
86 # Taken from the Zope service support - each "child" is run as a sub-process
87 # (trying to run multiple twisted apps in the same process is likely to screw
88 # stdout redirection etc).
89 # Note that unlike the Zope service, we do *not* attempt to detect a failed
90 # client and perform restarts - buildbot itself does a good job
91 # at reconnecting, and Windows itself provides restart semantics should
92 # everything go pear-shaped.
94 # We execute a new thread that captures the tail of the output from our child
95 # process. If the child fails, it is written to the event log.
96 # This process is unconditional, and the output is never written to disk
97 # (except obviously via the event log entry)
98 # Size of the blocks we read from the child process's output.
99 CHILDCAPTURE_BLOCK_SIZE
= 80
100 # The number of BLOCKSIZE blocks we keep as process output.
101 CHILDCAPTURE_MAX_BLOCKS
= 200
104 class BBService(win32serviceutil
.ServiceFramework
):
105 _svc_name_
= 'BuildBot'
106 _svc_display_name_
= _svc_name_
107 _svc_description_
= 'Manages local buildbot slaves and masters - ' \
108 'see http://buildbot.sourceforge.net'
110 def __init__(self
, args
):
111 win32serviceutil
.ServiceFramework
.__init
__(self
, args
)
113 # Create an event which we will use to wait on. The "service stop"
114 # request will set this event.
115 # * We must make it inheritable so we can pass it to the child
116 # process via the cmd-line
117 # * Must be manual reset so each child process and our service
118 # all get woken from a single set of the event.
119 sa
= win32security
.SECURITY_ATTRIBUTES()
120 sa
.bInheritHandle
= True
121 self
.hWaitStop
= win32event
.CreateEvent(sa
, True, False, None)
125 self
.runner_prefix
= None
127 # Patch up the service messages file in a frozen exe.
128 # (We use the py2exe option that magically bundles the .pyd files
129 # into the .zip file - so servicemanager.pyd doesn't exist.)
130 if is_frozen
and servicemanager
.RunningAsService():
131 msg_file
= os
.path
.join(os
.path
.dirname(sys
.executable
),
133 if os
.path
.isfile(msg_file
):
134 servicemanager
.Initialize("BuildBot", msg_file
)
136 self
.warning("Strange - '%s' does not exist" % (msg_file
, ))
138 def _checkConfig(self
):
139 # Locate our child process runner (but only when run from source)
141 # Running from source
142 python_exe
= os
.path
.join(sys
.prefix
, "python.exe")
143 if not os
.path
.isfile(python_exe
):
144 # for ppl who build Python itself from source.
145 python_exe
= os
.path
.join(sys
.prefix
, "PCBuild", "python.exe")
146 if not os
.path
.isfile(python_exe
):
147 self
.error("Can not find python.exe to spawn subprocess")
151 if me
.endswith(".pyc") or me
.endswith(".pyo"):
154 self
.runner_prefix
= '"%s" "%s"' % (python_exe
, me
)
156 # Running from a py2exe built executable - our child process is
157 # us (but with the funky cmdline args!)
158 self
.runner_prefix
= '"' + sys
.executable
+ '"'
160 # Now our arg processing - this may be better handled by a
161 # twisted/buildbot style config file - but as of time of writing,
162 # MarkH is clueless about such things!
164 # Note that the "arguments" you type into Control Panel for the
165 # service do *not* persist - they apply only when you click "start"
166 # on the service. When started by Windows, args are never presented.
167 # Thus, it is the responsibility of the service to persist any args.
169 # so, when args are presented, we save them as a "custom option". If
170 # they are not presented, we load them from the option.
172 if len(self
.args
) > 1:
173 dir_string
= os
.pathsep
.join(self
.args
[1:])
176 dir_string
= win32serviceutil
.GetServiceCustomOption(self
,
181 self
.error("You must specify the buildbot directories as "
182 "parameters to the service.\nStopping the service.")
185 dirs
= dir_string
.split(os
.pathsep
)
187 d
= os
.path
.abspath(d
)
188 sentinal
= os
.path
.join(d
, "buildbot.tac")
189 if os
.path
.isfile(sentinal
):
192 msg
= "Directory '%s' is not a buildbot dir - ignoring" \
196 self
.error("No valid buildbot directories were specified.\n"
197 "Stopping the service.")
200 dir_string
= os
.pathsep
.join(self
.dirs
).encode("mbcs")
201 win32serviceutil
.SetServiceCustomOption(self
, "directories",
206 # Tell the SCM we are starting the stop process.
207 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
208 # Set the stop event - the main loop takes care of termination.
209 win32event
.SetEvent(self
.hWaitStop
)
211 # SvcStop only gets triggered when the user explictly stops (or restarts)
212 # the service. To shut the service down cleanly when Windows is shutting
213 # down, we also need to hook SvcShutdown.
214 SvcShutdown
= SvcStop
217 if not self
._checkConfig
():
218 # stopped status set by caller.
221 self
.logmsg(servicemanager
.PYS_SERVICE_STARTED
)
225 for bbdir
in self
.dirs
:
226 self
.info("Starting BuildBot in directory '%s'" % (bbdir
, ))
227 hstop
= self
.hWaitStop
229 cmd
= '%s --spawn %d start %s' % (self
.runner_prefix
, hstop
, bbdir
)
231 h
, t
, output
= self
.createProcess(cmd
)
232 child_infos
.append((bbdir
, h
, t
, output
))
235 handles
= [self
.hWaitStop
] + [i
[1] for i
in child_infos
]
237 rc
= win32event
.WaitForMultipleObjects(handles
,
240 if rc
== win32event
.WAIT_OBJECT_0
:
241 # user sent a stop service request
244 # A child process died. For now, just log the output
245 # and forget the process.
246 index
= rc
- win32event
.WAIT_OBJECT_0
- 1
247 bbdir
, dead_handle
, dead_thread
, output_blocks
= \
249 status
= win32process
.GetExitCodeProcess(dead_handle
)
250 output
= "".join(output_blocks
)
252 output
= "The child process generated no output. " \
253 "Please check the twistd.log file in the " \
254 "indicated directory."
256 self
.warning("BuildBot for directory %r terminated with "
257 "exit code %d.\n%s" % (bbdir
, status
, output
))
259 del child_infos
[index
]
262 self
.warning("All BuildBot child processes have "
263 "terminated. Service stopping.")
265 # Either no child processes left, or stop event set.
266 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
268 # The child processes should have also seen our stop signal
269 # so wait for them to terminate.
270 for bbdir
, h
, t
, output
in child_infos
:
271 for i
in range(10): # 30 seconds to shutdown...
272 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
273 rc
= win32event
.WaitForSingleObject(h
, 3000)
274 if rc
== win32event
.WAIT_OBJECT_0
:
276 # Process terminated - no need to try harder.
277 if rc
== win32event
.WAIT_OBJECT_0
:
280 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
281 # If necessary, kill it
282 if win32process
.GetExitCodeProcess(h
)==win32con
.STILL_ACTIVE
:
283 self
.warning("BuildBot process at %r failed to terminate - "
284 "killing it" % (bbdir
, ))
285 win32api
.TerminateProcess(h
, 3)
286 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
288 # Wait for the redirect thread - it should have died as the remote
289 # process terminated.
290 # As we are shutting down, we do the join with a little more care,
291 # reporting progress as we wait (even though we never will <wink>)
294 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
298 self
.warning("Redirect thread did not stop!")
301 self
.logmsg(servicemanager
.PYS_SERVICE_STOPPED
)
304 # Error reporting/logging functions.
307 def logmsg(self
, event
):
308 # log a service event using servicemanager.LogMsg
310 servicemanager
.LogMsg(servicemanager
.EVENTLOG_INFORMATION_TYPE
,
313 " (%s)" % self
._svc
_display
_name
_))
314 except win32api
.error
, details
:
315 # Failed to write a log entry - most likely problem is
316 # that the event log is full. We don't want this to kill us
318 print "FAILED to write INFO event", event
, ":", details
320 # No valid stdout! Ignore it.
323 def _dolog(self
, func
, msg
):
326 except win32api
.error
, details
:
327 # Failed to write a log entry - most likely problem is
328 # that the event log is full. We don't want this to kill us
330 print "FAILED to write event log entry:", details
336 self
._dolog
(servicemanager
.LogInfoMsg
, s
)
338 def warning(self
, s
):
339 self
._dolog
(servicemanager
.LogWarningMsg
, s
)
342 self
._dolog
(servicemanager
.LogErrorMsg
, s
)
344 # Functions that spawn a child process, redirecting any output.
345 # Although builtbot itself does this, it is very handy to debug issues
346 # such as ImportErrors that happen before buildbot has redirected.
348 def createProcess(self
, cmd
):
349 hInputRead
, hInputWriteTemp
= self
.newPipe()
350 hOutReadTemp
, hOutWrite
= self
.newPipe()
351 pid
= win32api
.GetCurrentProcess()
352 # This one is duplicated as inheritable.
353 hErrWrite
= win32api
.DuplicateHandle(pid
, hOutWrite
, pid
, 0, 1,
354 win32con
.DUPLICATE_SAME_ACCESS
)
356 # These are non-inheritable duplicates.
357 hOutRead
= self
.dup(hOutReadTemp
)
358 hInputWrite
= self
.dup(hInputWriteTemp
)
359 # dup() closed hOutReadTemp, hInputWriteTemp
361 si
= win32process
.STARTUPINFO()
362 si
.hStdInput
= hInputRead
363 si
.hStdOutput
= hOutWrite
364 si
.hStdError
= hErrWrite
365 si
.dwFlags
= win32process
.STARTF_USESTDHANDLES | \
366 win32process
.STARTF_USESHOWWINDOW
367 si
.wShowWindow
= win32con
.SW_HIDE
369 # pass True to allow handles to be inherited. Inheritance is
370 # problematic in general, but should work in the controlled
371 # circumstances of a service process.
372 create_flags
= win32process
.CREATE_NEW_CONSOLE
373 # info is (hProcess, hThread, pid, tid)
374 info
= win32process
.CreateProcess(None, cmd
, None, None, True,
375 create_flags
, None, None, si
)
376 # (NOTE: these really aren't necessary for Python - they are closed
377 # as soon as they are collected)
384 # start a thread collecting output
386 t
= threading
.Thread(target
=self
.redirectCaptureThread
,
387 args
= (hOutRead
, blocks
))
389 return info
[0], t
, blocks
391 def redirectCaptureThread(self
, handle
, captured_blocks
):
392 # One of these running per child process we are watching. It
393 # handles both stdout and stderr on a single handle. The read data is
394 # never referenced until the thread dies - so no need for locks
395 # around self.captured_blocks.
396 #self.info("Redirect thread starting")
399 ec
, data
= win32file
.ReadFile(handle
, CHILDCAPTURE_BLOCK_SIZE
)
400 except pywintypes
.error
, err
:
401 # ERROR_BROKEN_PIPE means the child process closed the
402 # handle - ie, it terminated.
403 if err
[0] != winerror
.ERROR_BROKEN_PIPE
:
404 self
.warning("Error reading output from process: %s" % err
)
406 captured_blocks
.append(data
)
407 del captured_blocks
[CHILDCAPTURE_MAX_BLOCKS
:]
409 #self.info("Redirect capture thread terminating")
412 sa
= win32security
.SECURITY_ATTRIBUTES()
413 sa
.bInheritHandle
= True
414 return win32pipe
.CreatePipe(sa
, 0)
417 # create a duplicate handle that is not inherited, so that
418 # it can be closed in the parent. close the original pipe in
420 pid
= win32api
.GetCurrentProcess()
421 dup
= win32api
.DuplicateHandle(pid
, pipe
, pid
, 0, 0,
422 win32con
.DUPLICATE_SAME_ACCESS
)
427 # Service registration and startup
430 def RegisterWithFirewall(exe_name
, description
):
431 # Register our executable as an exception with Windows Firewall.
432 # taken from http://msdn.microsoft.com/library/default.asp?url=\
433 #/library/en-us/ics/ics/wf_adding_an_application.asp
434 from win32com
.client
import Dispatch
436 NET_FW_PROFILE_DOMAIN
= 0
437 NET_FW_PROFILE_STANDARD
= 1
442 # IP Version - ANY is the only allowable setting for now
443 NET_FW_IP_VERSION_ANY
= 2
445 fwMgr
= Dispatch("HNetCfg.FwMgr")
447 # Get the current profile for the local firewall policy.
448 profile
= fwMgr
.LocalPolicy
.CurrentProfile
450 app
= Dispatch("HNetCfg.FwAuthorizedApplication")
452 app
.ProcessImageFileName
= exe_name
453 app
.Name
= description
454 app
.Scope
= NET_FW_SCOPE_ALL
455 # Use either Scope or RemoteAddresses, but not both
456 #app.RemoteAddresses = "*"
457 app
.IpVersion
= NET_FW_IP_VERSION_ANY
460 # Use this line if you want to add the app, but disabled.
463 profile
.AuthorizedApplications
.Add(app
)
466 # A custom install function.
469 def CustomInstall(opts
):
470 # Register this process with the Windows Firewaall
473 RegisterWithFirewall(sys
.executable
, "BuildBot")
474 except pythoncom
.com_error
, why
:
475 print "FAILED to register with the Windows firewall"
479 # Magic code to allow shutdown. Note that this code is executed in
480 # the *child* process, by way of the service process executing us with
481 # special cmdline args (which includes the service stop handle!)
485 del sys
.argv
[1] # The --spawn arg.
486 # Create a new thread that just waits for the event to be signalled.
487 t
= threading
.Thread(target
=_WaitForShutdown
,
488 args
= (int(sys
.argv
[1]), )
490 del sys
.argv
[1] # The stop handle
491 # This child process will be sent a console handler notification as
492 # users log off, or as the system shuts down. We want to ignore these
493 # signals as the service parent is responsible for our shutdown.
495 def ConsoleHandler(what
):
496 # We can ignore *everything* - ctrl+c will never be sent as this
497 # process is never attached to a console the user can press the
500 win32api
.SetConsoleCtrlHandler(ConsoleHandler
, True)
501 t
.setDaemon(True) # we don't want to wait for this to stop!
503 if hasattr(sys
, "frozen"):
504 # py2exe sets this env vars that may screw our child process - reset
505 del os
.environ
["PYTHONPATH"]
507 # Start the buildbot app
508 from buildbot
.scripts
import runner
510 print "Service child process terminating normally."
513 def _WaitForShutdown(h
):
514 win32event
.WaitForSingleObject(h
, win32event
.INFINITE
)
515 print "Shutdown requested"
517 from twisted
.internet
import reactor
518 reactor
.callLater(0, reactor
.stop
)
521 # This function is also called by the py2exe startup code.
524 def HandleCommandLine():
525 if len(sys
.argv
)>1 and sys
.argv
[1] == "--spawn":
526 # Special command-line created by the service to execute the
528 # First arg is the handle to wait on
531 win32serviceutil
.HandleCommandLine(BBService
,
532 customOptionHandler
=CustomInstall
)
535 if __name__
== '__main__':