more NEWS items
[buildbot.git] / contrib / windows / buildbot_service.py
blob50bc1476e3fabb72fe257537e678be891af3c138
1 # Runs the build-bot as a Windows service.
2 # To use:
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.
17 # For example:
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
23 # to configure it.
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"
29 # or:
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
51 # Troubleshooting:
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.
55 # * Try executing:
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
68 import pywintypes
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)
112 self.args = args
113 self.dirs = 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),
121 "buildbot.msg")
122 if os.path.isfile(msg_file):
123 servicemanager.Initialize("BuildBot", msg_file)
124 else:
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)
129 if not is_frozen:
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")
137 return False
139 me = __file__
140 if me.endswith(".pyc") or me.endswith(".pyo"):
141 me = me[:-1]
143 self.runner_prefix = '"%s" "%s"' % (python_exe, me)
144 else:
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.
160 self.dirs = []
161 if len(self.args) > 1:
162 dir_string = os.pathsep.join(self.args[1:])
163 save_dirs = True
164 else:
165 dir_string = win32serviceutil.GetServiceCustomOption(self,
166 "directories")
167 save_dirs = False
169 if not dir_string:
170 self.error("You must specify the buildbot directories as "
171 "parameters to the service.\nStopping the service.")
172 return False
174 dirs = dir_string.split(os.pathsep)
175 for d in dirs:
176 d = os.path.abspath(d)
177 sentinal = os.path.join(d, "buildbot.tac")
178 if os.path.isfile(sentinal):
179 self.dirs.append(d)
180 else:
181 msg = "Directory '%s' is not a buildbot dir - ignoring" \
182 % (d,)
183 self.warning(msg)
184 if not self.dirs:
185 self.error("No valid buildbot directories were specified.\n"
186 "Stopping the service.")
187 return False
188 if save_dirs:
189 dir_string = os.pathsep.join(self.dirs).encode("mbcs")
190 win32serviceutil.SetServiceCustomOption(self, "directories",
191 dir_string)
192 return True
194 def SvcStop(self):
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
205 def SvcDoRun(self):
206 if not self._checkConfig():
207 # stopped status set by caller.
208 return
210 self.logmsg(servicemanager.PYS_SERVICE_STARTED)
212 child_infos = []
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)
219 #print "cmd is", cmd
220 h, t, output = self.createProcess(cmd)
221 child_infos.append((bbdir, h, t, output))
223 while child_infos:
224 handles = [self.hWaitStop] + [i[1] for i in child_infos]
226 rc = win32event.WaitForMultipleObjects(handles,
227 0, # bWaitAll
228 win32event.INFINITE)
229 if rc == win32event.WAIT_OBJECT_0:
230 # user sent a stop service request
231 break
232 else:
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 = \
237 child_infos[index]
238 status = win32process.GetExitCodeProcess(dead_handle)
239 output = "".join(output_blocks)
240 if not output:
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]
250 if not child_infos:
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:
264 break
265 # Process terminated - no need to try harder.
266 if rc == win32event.WAIT_OBJECT_0:
267 break
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>)
281 for i in range(5):
282 t.join(1)
283 self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
284 if not t.isAlive():
285 break
286 else:
287 self.warning("Redirect thread did not stop!")
289 # All done.
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
297 try:
298 servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
299 event,
300 (self._svc_name_,
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
305 try:
306 print "FAILED to write INFO event", event, ":", details
307 except IOError:
308 # No valid stdout! Ignore it.
309 pass
311 def _dolog(self, func, msg):
312 try:
313 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
317 try:
318 print "FAILED to write event log entry:", details
319 print msg
320 except IOError:
321 pass
323 def info(self, s):
324 self._dolog(servicemanager.LogInfoMsg, s)
326 def warning(self, s):
327 self._dolog(servicemanager.LogWarningMsg, s)
329 def error(self, 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)
365 hOutWrite.Close()
366 hErrWrite.Close()
367 hInputRead.Close()
368 # We don't use stdin
369 hInputWrite.Close()
371 # start a thread collecting output
372 blocks = []
373 t = threading.Thread(target=self.redirectCaptureThread,
374 args = (hOutRead,blocks))
375 t.start()
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")
384 while 1:
385 try:
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)
392 break
393 captured_blocks.append(data)
394 del captured_blocks[CHILDCAPTURE_MAX_BLOCKS:]
395 handle.Close()
396 #self.info("Redirect capture thread terminating")
398 def newPipe(self):
399 sa = win32security.SECURITY_ATTRIBUTES()
400 sa.bInheritHandle = True
401 return win32pipe.CreatePipe(sa, 0)
403 def dup(self, pipe):
404 # create a duplicate handle that is not inherited, so that
405 # it can be closed in the parent. close the original pipe in
406 # the process.
407 pid = win32api.GetCurrentProcess()
408 dup = win32api.DuplicateHandle(pid, pipe, pid, 0, 0,
409 win32con.DUPLICATE_SAME_ACCESS)
410 pipe.Close()
411 return dup
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
418 # Set constants
419 NET_FW_PROFILE_DOMAIN = 0
420 NET_FW_PROFILE_STANDARD = 1
422 # Scope
423 NET_FW_SCOPE_ALL = 0
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
441 app.Enabled = True
443 # Use this line if you want to add the app, but disabled.
444 #app.Enabled = False
446 profile.AuthorizedApplications.Add(app)
448 # A custom install function.
449 def CustomInstall(opts):
450 # Register this process with the Windows Firewaall
451 import pythoncom
452 try:
453 RegisterWithFirewall(sys.executable, "BuildBot")
454 except pythoncom.com_error, why:
455 print "FAILED to register with the Windows firewall"
456 print why
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!)
462 def _RunChild():
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
475 # key in!
476 return True
477 win32api.SetConsoleCtrlHandler(ConsoleHandler, True)
478 t.setDaemon(True) # we don't want to wait for this to stop!
479 t.start()
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
486 runner.run()
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
500 # child-process.
501 # First arg is the handle to wait on
502 _RunChild()
503 else:
504 win32serviceutil.HandleCommandLine(BBService,
505 customOptionHandler=CustomInstall)
507 if __name__ == '__main__':
508 HandleCommandLine()