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!)
484
def _RunChild(runfn):
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/buildslave app
509
print "Service child process terminating normally."
512
def _WaitForShutdown(h):
513
win32event.WaitForSingleObject(h, win32event.INFINITE)
514
print "Shutdown requested"
516
from twisted.internet import reactor
517
reactor.callLater(0, reactor.stop)
519
def DetermineRunner(bbdir):
520
'''Checks if the given directory is a buildslave or a master and returns the
521
appropriate run function.'''
523
import buildslave.scripts.runner
524
tacfile = os.path.join(bbdir, 'buildbot.tac')
526
if os.path.exists(tacfile):
527
with open(tacfile, 'r') as f:
529
if 'import BuildSlave' in contents:
530
return buildslave.scripts.runner.run
536
import buildbot.scripts.runner
537
return buildbot.scripts.runner.run
539
# This function is also called by the py2exe startup code.
542
def HandleCommandLine():
543
if len(sys.argv)>1 and sys.argv[1] == "--spawn":
544
# Special command-line created by the service to execute the
546
# First arg is the handle to wait on
547
# Fourth arg is the config directory to use for the buildbot/slave
548
_RunChild(DetermineRunner(sys.argv[4]))
550
win32serviceutil.HandleCommandLine(BBService,
551
customOptionHandler=CustomInstall)
554
if __name__ == '__main__':