jack2 codebase
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

403 lines
9.5KB

  1. #! /usr/bin/env python
  2. # encoding: utf-8
  3. # Thomas Nagy, 2015 (ita)
  4. """
  5. Execute commands through pre-forked servers. This tool creates as many servers as build threads.
  6. On a benchmark executed on Linux Kubuntu 14, 8 virtual cores and SSD drive::
  7. ./genbench.py /tmp/build 200 100 15 5
  8. waf clean build -j24
  9. # no prefork: 2m7.179s
  10. # prefork: 0m55.400s
  11. To use::
  12. def options(opt):
  13. # optional, will spawn 40 servers early
  14. opt.load('prefork')
  15. def build(bld):
  16. bld.load('prefork')
  17. ...
  18. more code
  19. The servers and the build process are using a shared nonce to prevent undesirable external connections.
  20. """
  21. import os, re, socket, threading, sys, subprocess, time, atexit, traceback, random, signal
  22. try:
  23. import SocketServer
  24. except ImportError:
  25. import socketserver as SocketServer
  26. try:
  27. from queue import Queue
  28. except ImportError:
  29. from Queue import Queue
  30. try:
  31. import cPickle
  32. except ImportError:
  33. import pickle as cPickle
  34. SHARED_KEY = None
  35. HEADER_SIZE = 64
  36. REQ = 'REQ'
  37. RES = 'RES'
  38. BYE = 'BYE'
  39. def make_header(params, cookie=''):
  40. header = ','.join(params)
  41. header = header.ljust(HEADER_SIZE - len(cookie))
  42. assert(len(header) == HEADER_SIZE - len(cookie))
  43. header = header + cookie
  44. if sys.hexversion > 0x3000000:
  45. header = header.encode('iso8859-1')
  46. return header
  47. def safe_compare(x, y):
  48. sum = 0
  49. for (a, b) in zip(x, y):
  50. sum |= ord(a) ^ ord(b)
  51. return sum == 0
  52. re_valid_query = re.compile('^[a-zA-Z0-9_, ]+$')
  53. class req(SocketServer.StreamRequestHandler):
  54. def handle(self):
  55. try:
  56. while self.process_command():
  57. pass
  58. except KeyboardInterrupt:
  59. return
  60. except Exception as e:
  61. print(e)
  62. def send_response(self, ret, out, err, exc):
  63. if out or err or exc:
  64. data = (out, err, exc)
  65. data = cPickle.dumps(data, -1)
  66. else:
  67. data = ''
  68. params = [RES, str(ret), str(len(data))]
  69. # no need for the cookie in the response
  70. self.wfile.write(make_header(params))
  71. if data:
  72. self.wfile.write(data)
  73. self.wfile.flush()
  74. def process_command(self):
  75. query = self.rfile.read(HEADER_SIZE)
  76. if not query:
  77. return None
  78. #print(len(query))
  79. assert(len(query) == HEADER_SIZE)
  80. if sys.hexversion > 0x3000000:
  81. query = query.decode('iso8859-1')
  82. # magic cookie
  83. key = query[-20:]
  84. if not safe_compare(key, SHARED_KEY):
  85. print('%r %r' % (key, SHARED_KEY))
  86. self.send_response(-1, '', '', 'Invalid key given!')
  87. return 'meh'
  88. query = query[:-20]
  89. #print "%r" % query
  90. if not re_valid_query.match(query):
  91. self.send_response(-1, '', '', 'Invalid query %r' % query)
  92. raise ValueError('Invalid query %r' % query)
  93. query = query.strip().split(',')
  94. if query[0] == REQ:
  95. self.run_command(query[1:])
  96. elif query[0] == BYE:
  97. raise ValueError('Exit')
  98. else:
  99. raise ValueError('Invalid query %r' % query)
  100. return 'ok'
  101. def run_command(self, query):
  102. size = int(query[0])
  103. data = self.rfile.read(size)
  104. assert(len(data) == size)
  105. kw = cPickle.loads(data)
  106. # run command
  107. ret = out = err = exc = None
  108. cmd = kw['cmd']
  109. del kw['cmd']
  110. #print(cmd)
  111. try:
  112. if kw['stdout'] or kw['stderr']:
  113. p = subprocess.Popen(cmd, **kw)
  114. (out, err) = p.communicate()
  115. ret = p.returncode
  116. else:
  117. ret = subprocess.Popen(cmd, **kw).wait()
  118. except KeyboardInterrupt:
  119. raise
  120. except Exception as e:
  121. ret = -1
  122. exc = str(e) + traceback.format_exc()
  123. self.send_response(ret, out, err, exc)
  124. def create_server(conn, cls):
  125. # child processes do not need the key, so we remove it from the OS environment
  126. global SHARED_KEY
  127. SHARED_KEY = os.environ['SHARED_KEY']
  128. os.environ['SHARED_KEY'] = ''
  129. ppid = int(os.environ['PREFORKPID'])
  130. def reap():
  131. if os.sep != '/':
  132. os.waitpid(ppid, 0)
  133. else:
  134. while 1:
  135. try:
  136. os.kill(ppid, 0)
  137. except OSError:
  138. break
  139. else:
  140. time.sleep(1)
  141. os.kill(os.getpid(), signal.SIGKILL)
  142. t = threading.Thread(target=reap)
  143. t.setDaemon(True)
  144. t.start()
  145. server = SocketServer.TCPServer(conn, req)
  146. print(server.server_address[1])
  147. sys.stdout.flush()
  148. #server.timeout = 6000 # seconds
  149. server.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
  150. try:
  151. server.serve_forever(poll_interval=0.001)
  152. except KeyboardInterrupt:
  153. pass
  154. if __name__ == '__main__':
  155. conn = ("127.0.0.1", 0)
  156. #print("listening - %r %r\n" % conn)
  157. create_server(conn, req)
  158. else:
  159. from waflib import Logs, Utils, Runner, Errors, Options
  160. def init_task_pool(self):
  161. # lazy creation, and set a common pool for all task consumers
  162. pool = self.pool = []
  163. for i in range(self.numjobs):
  164. consumer = Runner.get_pool()
  165. pool.append(consumer)
  166. consumer.idx = i
  167. self.ready = Queue(0)
  168. def setq(consumer):
  169. consumer.ready = self.ready
  170. try:
  171. threading.current_thread().idx = consumer.idx
  172. except Exception as e:
  173. print(e)
  174. for x in pool:
  175. x.ready.put(setq)
  176. return pool
  177. Runner.Parallel.init_task_pool = init_task_pool
  178. def make_server(bld, idx):
  179. cmd = [sys.executable, os.path.abspath(__file__)]
  180. proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
  181. return proc
  182. def make_conn(bld, srv):
  183. port = srv.port
  184. conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  185. conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
  186. conn.connect(('127.0.0.1', port))
  187. return conn
  188. SERVERS = []
  189. CONNS = []
  190. def close_all():
  191. global SERVERS, CONNS
  192. while CONNS:
  193. conn = CONNS.pop()
  194. try:
  195. conn.close()
  196. except:
  197. pass
  198. while SERVERS:
  199. srv = SERVERS.pop()
  200. try:
  201. srv.kill()
  202. except:
  203. pass
  204. atexit.register(close_all)
  205. def put_data(conn, data):
  206. cnt = 0
  207. while cnt < len(data):
  208. sent = conn.send(data[cnt:])
  209. if sent == 0:
  210. raise RuntimeError('connection ended')
  211. cnt += sent
  212. def read_data(conn, siz):
  213. cnt = 0
  214. buf = []
  215. while cnt < siz:
  216. data = conn.recv(min(siz - cnt, 1024))
  217. if not data:
  218. raise RuntimeError('connection ended %r %r' % (cnt, siz))
  219. buf.append(data)
  220. cnt += len(data)
  221. if sys.hexversion > 0x3000000:
  222. ret = ''.encode('iso8859-1').join(buf)
  223. else:
  224. ret = ''.join(buf)
  225. return ret
  226. def exec_command(self, cmd, **kw):
  227. if 'stdout' in kw:
  228. if kw['stdout'] not in (None, subprocess.PIPE):
  229. return self.exec_command_old(cmd, **kw)
  230. elif 'stderr' in kw:
  231. if kw['stderr'] not in (None, subprocess.PIPE):
  232. return self.exec_command_old(cmd, **kw)
  233. kw['shell'] = isinstance(cmd, str)
  234. Logs.debug('runner: %r' % cmd)
  235. Logs.debug('runner_env: kw=%s' % kw)
  236. if self.logger:
  237. self.logger.info(cmd)
  238. if 'stdout' not in kw:
  239. kw['stdout'] = subprocess.PIPE
  240. if 'stderr' not in kw:
  241. kw['stderr'] = subprocess.PIPE
  242. if Logs.verbose and not kw['shell'] and not Utils.check_exe(cmd[0]):
  243. raise Errors.WafError("Program %s not found!" % cmd[0])
  244. idx = threading.current_thread().idx
  245. kw['cmd'] = cmd
  246. # serialization..
  247. #print("sub %r %r" % (idx, cmd))
  248. #print("write to %r %r" % (idx, cmd))
  249. data = cPickle.dumps(kw, -1)
  250. params = [REQ, str(len(data))]
  251. header = make_header(params, self.SHARED_KEY)
  252. conn = CONNS[idx]
  253. put_data(conn, header + data)
  254. #put_data(conn, data)
  255. #print("running %r %r" % (idx, cmd))
  256. #print("read from %r %r" % (idx, cmd))
  257. data = read_data(conn, HEADER_SIZE)
  258. if sys.hexversion > 0x3000000:
  259. data = data.decode('iso8859-1')
  260. #print("received %r" % data)
  261. lst = data.split(',')
  262. ret = int(lst[1])
  263. dlen = int(lst[2])
  264. out = err = None
  265. if dlen:
  266. data = read_data(conn, dlen)
  267. (out, err, exc) = cPickle.loads(data)
  268. if exc:
  269. raise Errors.WafError('Execution failure: %s' % exc)
  270. if out:
  271. if not isinstance(out, str):
  272. out = out.decode(sys.stdout.encoding or 'iso8859-1')
  273. if self.logger:
  274. self.logger.debug('out: %s' % out)
  275. else:
  276. Logs.info(out, extra={'stream':sys.stdout, 'c1': ''})
  277. if err:
  278. if not isinstance(err, str):
  279. err = err.decode(sys.stdout.encoding or 'iso8859-1')
  280. if self.logger:
  281. self.logger.error('err: %s' % err)
  282. else:
  283. Logs.info(err, extra={'stream':sys.stderr, 'c1': ''})
  284. return ret
  285. def init_key(ctx):
  286. try:
  287. key = ctx.SHARED_KEY = os.environ['SHARED_KEY']
  288. except KeyError:
  289. key = "".join([chr(random.SystemRandom().randint(40, 126)) for x in range(20)])
  290. os.environ['SHARED_KEY'] = ctx.SHARED_KEY = key
  291. os.environ['PREFORKPID'] = str(os.getpid())
  292. return key
  293. def init_servers(ctx, maxval):
  294. while len(SERVERS) < maxval:
  295. i = len(SERVERS)
  296. srv = make_server(ctx, i)
  297. SERVERS.append(srv)
  298. while len(CONNS) < maxval:
  299. i = len(CONNS)
  300. srv = SERVERS[i]
  301. # postpone the connection
  302. srv.port = int(srv.stdout.readline())
  303. conn = None
  304. for x in range(30):
  305. try:
  306. conn = make_conn(ctx, srv)
  307. break
  308. except socket.error:
  309. time.sleep(0.01)
  310. if not conn:
  311. raise ValueError('Could not start the server!')
  312. if srv.poll() is not None:
  313. Logs.warn('Looks like it it not our server process - concurrent builds are unsupported at this stage')
  314. raise ValueError('Could not start the server')
  315. CONNS.append(conn)
  316. def init_smp(self):
  317. if not getattr(Options.options, 'smp', getattr(self, 'smp', None)):
  318. return
  319. if Utils.unversioned_sys_platform() in ('freebsd',):
  320. pid = os.getpid()
  321. cmd = ['cpuset', '-l', '0', '-p', str(pid)]
  322. elif Utils.unversioned_sys_platform() in ('linux',):
  323. pid = os.getpid()
  324. cmd = ['taskset', '-pc', '0', str(pid)]
  325. if cmd:
  326. self.cmd_and_log(cmd, quiet=0)
  327. def options(opt):
  328. init_key(opt)
  329. init_servers(opt, 40)
  330. opt.add_option('--pin-process', action='store_true', dest='smp', default=False)
  331. def build(bld):
  332. if bld.cmd == 'clean':
  333. return
  334. init_key(bld)
  335. init_servers(bld, bld.jobs)
  336. init_smp(bld)
  337. bld.__class__.exec_command_old = bld.__class__.exec_command
  338. bld.__class__.exec_command = exec_command