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.
66 import sys
, os
, threading
69 import winerror
, win32con
70 import win32api
, win32event
, win32file
, win32pipe
, win32process
, win32security
71 import win32service
, win32serviceutil
, servicemanager
73 # Are we running in a py2exe environment?
74 is_frozen
= hasattr(sys
, "frozen")
76 # Taken from the Zope service support - each "child" is run as a sub-process
77 # (trying to run multiple twisted apps in the same process is likely to screw
78 # stdout redirection etc).
79 # Note that unlike the Zope service, we do *not* attempt to detect a failed
80 # client and perform restarts - buildbot itself does a good job
81 # at reconnecting, and Windows itself provides restart semantics should
82 # everything go pear-shaped.
84 # We execute a new thread that captures the tail of the output from our child
85 # process. If the child fails, it is written to the event log.
86 # This process is unconditional, and the output is never written to disk
87 # (except obviously via the event log entry)
88 # Size of the blocks we read from the child process's output.
89 CHILDCAPTURE_BLOCK_SIZE
= 80
90 # The number of BLOCKSIZE blocks we keep as process output.
91 CHILDCAPTURE_MAX_BLOCKS
= 200
93 class BBService(win32serviceutil
.ServiceFramework
):
94 _svc_name_
= 'BuildBot'
95 _svc_display_name_
= _svc_name_
96 _svc_description_
= 'Manages local buildbot slaves and masters - ' \
97 'see http://buildbot.sourceforge.net'
99 def __init__(self
, args
):
100 win32serviceutil
.ServiceFramework
.__init
__(self
, args
)
102 # Create an event which we will use to wait on. The "service stop"
103 # request will set this event.
104 # * We must make it inheritable so we can pass it to the child
105 # process via the cmd-line
106 # * Must be manual reset so each child process and our service
107 # all get woken from a single set of the event.
108 sa
= win32security
.SECURITY_ATTRIBUTES()
109 sa
.bInheritHandle
= True
110 self
.hWaitStop
= win32event
.CreateEvent(sa
, True, False, None)
114 self
.runner_prefix
= None
116 # Patch up the service messages file in a frozen exe.
117 # (We use the py2exe option that magically bundles the .pyd files
118 # into the .zip file - so servicemanager.pyd doesn't exist.)
119 if is_frozen
and servicemanager
.RunningAsService():
120 msg_file
= os
.path
.join(os
.path
.dirname(sys
.executable
),
122 if os
.path
.isfile(msg_file
):
123 servicemanager
.Initialize("BuildBot", msg_file
)
125 self
.warning("Strange - '%s' does not exist" % (msg_file
,))
127 def _checkConfig(self
):
128 # Locate our child process runner (but only when run from source)
130 # Running from source
131 python_exe
= os
.path
.join(sys
.prefix
, "python.exe")
132 if not os
.path
.isfile(python_exe
):
133 # for ppl who build Python itself from source.
134 python_exe
= os
.path
.join(sys
.prefix
, "PCBuild", "python.exe")
135 if not os
.path
.isfile(python_exe
):
136 self
.error("Can not find python.exe to spawn subprocess")
140 if me
.endswith(".pyc") or me
.endswith(".pyo"):
143 self
.runner_prefix
= '"%s" "%s"' % (python_exe
, me
)
145 # Running from a py2exe built executable - our child process is
146 # us (but with the funky cmdline args!)
147 self
.runner_prefix
= '"' + sys
.executable
+ '"'
149 # Now our arg processing - this may be better handled by a
150 # twisted/buildbot style config file - but as of time of writing,
151 # MarkH is clueless about such things!
153 # Note that the "arguments" you type into Control Panel for the
154 # service do *not* persist - they apply only when you click "start"
155 # on the service. When started by Windows, args are never presented.
156 # Thus, it is the responsibility of the service to persist any args.
158 # so, when args are presented, we save them as a "custom option". If
159 # they are not presented, we load them from the option.
161 if len(self
.args
) > 1:
162 dir_string
= os
.pathsep
.join(self
.args
[1:])
165 dir_string
= win32serviceutil
.GetServiceCustomOption(self
,
170 self
.error("You must specify the buildbot directories as "
171 "parameters to the service.\nStopping the service.")
174 dirs
= dir_string
.split(os
.pathsep
)
176 d
= os
.path
.abspath(d
)
177 sentinal
= os
.path
.join(d
, "buildbot.tac")
178 if os
.path
.isfile(sentinal
):
181 msg
= "Directory '%s' is not a buildbot dir - ignoring" \
185 self
.error("No valid buildbot directories were specified.\n"
186 "Stopping the service.")
189 dir_string
= os
.pathsep
.join(self
.dirs
).encode("mbcs")
190 win32serviceutil
.SetServiceCustomOption(self
, "directories",
195 # Tell the SCM we are starting the stop process.
196 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
197 # Set the stop event - the main loop takes care of termination.
198 win32event
.SetEvent(self
.hWaitStop
)
200 # SvcStop only gets triggered when the user explictly stops (or restarts)
201 # the service. To shut the service down cleanly when Windows is shutting
202 # down, we also need to hook SvcShutdown.
203 SvcShutdown
= SvcStop
206 if not self
._checkConfig
():
207 # stopped status set by caller.
210 self
.logmsg(servicemanager
.PYS_SERVICE_STARTED
)
214 for bbdir
in self
.dirs
:
215 self
.info("Starting BuildBot in directory '%s'" % (bbdir
,))
216 hstop
= self
.hWaitStop
218 cmd
= '%s --spawn %d start %s' % (self
.runner_prefix
, hstop
, bbdir
)
220 h
, t
, output
= self
.createProcess(cmd
)
221 child_infos
.append((bbdir
, h
, t
, output
))
224 handles
= [self
.hWaitStop
] + [i
[1] for i
in child_infos
]
226 rc
= win32event
.WaitForMultipleObjects(handles
,
229 if rc
== win32event
.WAIT_OBJECT_0
:
230 # user sent a stop service request
233 # A child process died. For now, just log the output
234 # and forget the process.
235 index
= rc
- win32event
.WAIT_OBJECT_0
- 1
236 bbdir
, dead_handle
, dead_thread
, output_blocks
= \
238 status
= win32process
.GetExitCodeProcess(dead_handle
)
239 output
= "".join(output_blocks
)
241 output
= "The child process generated no output. " \
242 "Please check the twistd.log file in the " \
243 "indicated directory."
245 self
.warning("BuildBot for directory %r terminated with "
246 "exit code %d.\n%s" % (bbdir
, status
, output
))
248 del child_infos
[index
]
251 self
.warning("All BuildBot child processes have "
252 "terminated. Service stopping.")
254 # Either no child processes left, or stop event set.
255 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
257 # The child processes should have also seen our stop signal
258 # so wait for them to terminate.
259 for bbdir
, h
, t
, output
in child_infos
:
260 for i
in range(10): # 30 seconds to shutdown...
261 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
262 rc
= win32event
.WaitForSingleObject(h
, 3000)
263 if rc
== win32event
.WAIT_OBJECT_0
:
265 # Process terminated - no need to try harder.
266 if rc
== win32event
.WAIT_OBJECT_0
:
269 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
270 # If necessary, kill it
271 if win32process
.GetExitCodeProcess(h
)==win32con
.STILL_ACTIVE
:
272 self
.warning("BuildBot process at %r failed to terminate - "
273 "killing it" % (bbdir
,))
274 win32api
.TerminateProcess(h
, 3)
275 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
277 # Wait for the redirect thread - it should have died as the remote
278 # process terminated.
279 # As we are shutting down, we do the join with a little more care,
280 # reporting progress as we wait (even though we never will <wink>)
283 self
.ReportServiceStatus(win32service
.SERVICE_STOP_PENDING
)
287 self
.warning("Redirect thread did not stop!")
290 self
.logmsg(servicemanager
.PYS_SERVICE_STOPPED
)
293 # Error reporting/logging functions.
295 def logmsg(self
, event
):
296 # log a service event using servicemanager.LogMsg
298 servicemanager
.LogMsg(servicemanager
.EVENTLOG_INFORMATION_TYPE
,
301 " (%s)" % self
._svc
_display
_name
_))
302 except win32api
.error
, details
:
303 # Failed to write a log entry - most likely problem is
304 # that the event log is full. We don't want this to kill us
306 print "FAILED to write INFO event", event
, ":", details
308 # No valid stdout! Ignore it.
311 def _dolog(self
, func
, msg
):
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 event log entry:", details
324 self
._dolog
(servicemanager
.LogInfoMsg
, s
)
326 def warning(self
, s
):
327 self
._dolog
(servicemanager
.LogWarningMsg
, s
)
330 self
._dolog
(servicemanager
.LogErrorMsg
, s
)
332 # Functions that spawn a child process, redirecting any output.
333 # Although builtbot itself does this, it is very handy to debug issues
334 # such as ImportErrors that happen before buildbot has redirected.
335 def createProcess(self
, cmd
):
336 hInputRead
, hInputWriteTemp
= self
.newPipe()
337 hOutReadTemp
, hOutWrite
= self
.newPipe()
338 pid
= win32api
.GetCurrentProcess()
339 # This one is duplicated as inheritable.
340 hErrWrite
= win32api
.DuplicateHandle(pid
, hOutWrite
, pid
, 0, 1,
341 win32con
.DUPLICATE_SAME_ACCESS
)
343 # These are non-inheritable duplicates.
344 hOutRead
= self
.dup(hOutReadTemp
)
345 hInputWrite
= self
.dup(hInputWriteTemp
)
346 # dup() closed hOutReadTemp, hInputWriteTemp
348 si
= win32process
.STARTUPINFO()
349 si
.hStdInput
= hInputRead
350 si
.hStdOutput
= hOutWrite
351 si
.hStdError
= hErrWrite
352 si
.dwFlags
= win32process
.STARTF_USESTDHANDLES | \
353 win32process
.STARTF_USESHOWWINDOW
354 si
.wShowWindow
= win32con
.SW_HIDE
356 # pass True to allow handles to be inherited. Inheritance is
357 # problematic in general, but should work in the controlled
358 # circumstances of a service process.
359 create_flags
= win32process
.CREATE_NEW_CONSOLE
360 # info is (hProcess, hThread, pid, tid)
361 info
= win32process
.CreateProcess(None, cmd
, None, None, True,
362 create_flags
, None, None, si
)
363 # (NOTE: these really aren't necessary for Python - they are closed
364 # as soon as they are collected)
371 # start a thread collecting output
373 t
= threading
.Thread(target
=self
.redirectCaptureThread
,
374 args
= (hOutRead
,blocks
))
376 return info
[0], t
, blocks
378 def redirectCaptureThread(self
, handle
, captured_blocks
):
379 # One of these running per child process we are watching. It
380 # handles both stdout and stderr on a single handle. The read data is
381 # never referenced until the thread dies - so no need for locks
382 # around self.captured_blocks.
383 #self.info("Redirect thread starting")
386 ec
, data
= win32file
.ReadFile(handle
, CHILDCAPTURE_BLOCK_SIZE
)
387 except pywintypes
.error
, err
:
388 # ERROR_BROKEN_PIPE means the child process closed the
389 # handle - ie, it terminated.
390 if err
[0] != winerror
.ERROR_BROKEN_PIPE
:
391 self
.warning("Error reading output from process: %s" % err
)
393 captured_blocks
.append(data
)
394 del captured_blocks
[CHILDCAPTURE_MAX_BLOCKS
:]
396 #self.info("Redirect capture thread terminating")
399 sa
= win32security
.SECURITY_ATTRIBUTES()
400 sa
.bInheritHandle
= True
401 return win32pipe
.CreatePipe(sa
, 0)
404 # create a duplicate handle that is not inherited, so that
405 # it can be closed in the parent. close the original pipe in
407 pid
= win32api
.GetCurrentProcess()
408 dup
= win32api
.DuplicateHandle(pid
, pipe
, pid
, 0, 0,
409 win32con
.DUPLICATE_SAME_ACCESS
)
413 # Service registration and startup
414 def RegisterWithFirewall(exe_name
, description
):
415 # Register our executable as an exception with Windows Firewall.
416 # taken from http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ics/ics/wf_adding_an_application.asp
417 from win32com
.client
import Dispatch
419 NET_FW_PROFILE_DOMAIN
= 0
420 NET_FW_PROFILE_STANDARD
= 1
425 # IP Version - ANY is the only allowable setting for now
426 NET_FW_IP_VERSION_ANY
= 2
428 fwMgr
= Dispatch("HNetCfg.FwMgr")
430 # Get the current profile for the local firewall policy.
431 profile
= fwMgr
.LocalPolicy
.CurrentProfile
433 app
= Dispatch("HNetCfg.FwAuthorizedApplication")
435 app
.ProcessImageFileName
= exe_name
436 app
.Name
= description
437 app
.Scope
= NET_FW_SCOPE_ALL
438 # Use either Scope or RemoteAddresses, but not both
439 #app.RemoteAddresses = "*"
440 app
.IpVersion
= NET_FW_IP_VERSION_ANY
443 # Use this line if you want to add the app, but disabled.
446 profile
.AuthorizedApplications
.Add(app
)
448 # A custom install function.
449 def CustomInstall(opts
):
450 # Register this process with the Windows Firewaall
453 RegisterWithFirewall(sys
.executable
, "BuildBot")
454 except pythoncom
.com_error
, why
:
455 print "FAILED to register with the Windows firewall"
459 # Magic code to allow shutdown. Note that this code is executed in
460 # the *child* process, by way of the service process executing us with
461 # special cmdline args (which includes the service stop handle!)
463 del sys
.argv
[1] # The --spawn arg.
464 # Create a new thread that just waits for the event to be signalled.
465 t
= threading
.Thread(target
=_WaitForShutdown
,
466 args
= (int(sys
.argv
[1]),)
468 del sys
.argv
[1] # The stop handle
469 # This child process will be sent a console handler notification as
470 # users log off, or as the system shuts down. We want to ignore these
471 # signals as the service parent is responsible for our shutdown.
472 def ConsoleHandler(what
):
473 # We can ignore *everything* - ctrl+c will never be sent as this
474 # process is never attached to a console the user can press the
477 win32api
.SetConsoleCtrlHandler(ConsoleHandler
, True)
478 t
.setDaemon(True) # we don't want to wait for this to stop!
480 if hasattr(sys
, "frozen"):
481 # py2exe sets this env vars that may screw our child process - reset
482 del os
.environ
["PYTHONPATH"]
484 # Start the buildbot app
485 from buildbot
.scripts
import runner
487 print "Service child process terminating normally."
489 def _WaitForShutdown(h
):
490 win32event
.WaitForSingleObject(h
, win32event
.INFINITE
)
491 print "Shutdown requested"
493 from twisted
.internet
import reactor
494 reactor
.callLater(0, reactor
.stop
)
496 # This function is also called by the py2exe startup code.
497 def HandleCommandLine():
498 if len(sys
.argv
)>1 and sys
.argv
[1] == "--spawn":
499 # Special command-line created by the service to execute the
501 # First arg is the handle to wait on
504 win32serviceutil
.HandleCommandLine(BBService
,
505 customOptionHandler
=CustomInstall
)
507 if __name__
== '__main__':