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.

319 lines
7.4KB

  1. #! /usr/bin/env python
  2. # encoding: utf-8
  3. # Thomas Nagy, 2015 (ita)
  4. """
  5. A version of prefork.py that uses unix sockets. The advantage is that it does not expose
  6. connections to the outside. Yet performance it only works on unix-like systems
  7. and performance can be slightly worse.
  8. To use::
  9. def options(opt):
  10. # recommended, fork new processes before using more memory
  11. opt.load('preforkunix')
  12. def build(bld):
  13. bld.load('preforkunix')
  14. ...
  15. more code
  16. """
  17. import os, re, socket, threading, sys, subprocess, atexit, traceback, signal, time
  18. try:
  19. from queue import Queue
  20. except ImportError:
  21. from Queue import Queue
  22. try:
  23. import cPickle
  24. except ImportError:
  25. import pickle as cPickle
  26. HEADER_SIZE = 20
  27. REQ = 'REQ'
  28. RES = 'RES'
  29. BYE = 'BYE'
  30. def make_header(params, cookie=''):
  31. header = ','.join(params)
  32. header = header.ljust(HEADER_SIZE - len(cookie))
  33. assert(len(header) == HEADER_SIZE - len(cookie))
  34. header = header + cookie
  35. if sys.hexversion > 0x3000000:
  36. header = header.encode('iso8859-1')
  37. return header
  38. re_valid_query = re.compile('^[a-zA-Z0-9_, ]+$')
  39. if 1:
  40. def send_response(conn, ret, out, err, exc):
  41. if out or err or exc:
  42. data = (out, err, exc)
  43. data = cPickle.dumps(data, -1)
  44. else:
  45. data = ''
  46. params = [RES, str(ret), str(len(data))]
  47. # no need for the cookie in the response
  48. conn.send(make_header(params))
  49. if data:
  50. conn.send(data)
  51. def process_command(conn):
  52. query = conn.recv(HEADER_SIZE)
  53. if not query:
  54. return None
  55. #print(len(query))
  56. assert(len(query) == HEADER_SIZE)
  57. if sys.hexversion > 0x3000000:
  58. query = query.decode('iso8859-1')
  59. #print "%r" % query
  60. if not re_valid_query.match(query):
  61. send_response(conn, -1, '', '', 'Invalid query %r' % query)
  62. raise ValueError('Invalid query %r' % query)
  63. query = query.strip().split(',')
  64. if query[0] == REQ:
  65. run_command(conn, query[1:])
  66. elif query[0] == BYE:
  67. raise ValueError('Exit')
  68. else:
  69. raise ValueError('Invalid query %r' % query)
  70. return 'ok'
  71. def run_command(conn, query):
  72. size = int(query[0])
  73. data = conn.recv(size)
  74. assert(len(data) == size)
  75. kw = cPickle.loads(data)
  76. # run command
  77. ret = out = err = exc = None
  78. cmd = kw['cmd']
  79. del kw['cmd']
  80. #print(cmd)
  81. try:
  82. if kw['stdout'] or kw['stderr']:
  83. p = subprocess.Popen(cmd, **kw)
  84. (out, err) = p.communicate()
  85. ret = p.returncode
  86. else:
  87. ret = subprocess.Popen(cmd, **kw).wait()
  88. except KeyboardInterrupt:
  89. raise
  90. except Exception as e:
  91. ret = -1
  92. exc = str(e) + traceback.format_exc()
  93. send_response(conn, ret, out, err, exc)
  94. if 1:
  95. from waflib import Logs, Utils, Runner, Errors, Options
  96. def init_task_pool(self):
  97. # lazy creation, and set a common pool for all task consumers
  98. pool = self.pool = []
  99. for i in range(self.numjobs):
  100. consumer = Runner.get_pool()
  101. pool.append(consumer)
  102. consumer.idx = i
  103. self.ready = Queue(0)
  104. def setq(consumer):
  105. consumer.ready = self.ready
  106. try:
  107. threading.current_thread().idx = consumer.idx
  108. except Exception as e:
  109. print(e)
  110. for x in pool:
  111. x.ready.put(setq)
  112. return pool
  113. Runner.Parallel.init_task_pool = init_task_pool
  114. def make_conn(bld):
  115. child_socket, parent_socket = socket.socketpair(socket.AF_UNIX)
  116. ppid = os.getpid()
  117. pid = os.fork()
  118. if pid == 0:
  119. parent_socket.close()
  120. # if the parent crashes, try to exit cleanly
  121. def reap():
  122. while 1:
  123. try:
  124. os.kill(ppid, 0)
  125. except OSError:
  126. break
  127. else:
  128. time.sleep(1)
  129. os.kill(os.getpid(), signal.SIGKILL)
  130. t = threading.Thread(target=reap)
  131. t.setDaemon(True)
  132. t.start()
  133. # write to child_socket only
  134. try:
  135. while process_command(child_socket):
  136. pass
  137. except KeyboardInterrupt:
  138. sys.exit(2)
  139. else:
  140. child_socket.close()
  141. return (pid, parent_socket)
  142. SERVERS = []
  143. CONNS = []
  144. def close_all():
  145. global SERVERS, CONS
  146. while CONNS:
  147. conn = CONNS.pop()
  148. try:
  149. conn.close()
  150. except:
  151. pass
  152. while SERVERS:
  153. pid = SERVERS.pop()
  154. try:
  155. os.kill(pid, 9)
  156. except:
  157. pass
  158. atexit.register(close_all)
  159. def put_data(conn, data):
  160. cnt = 0
  161. while cnt < len(data):
  162. sent = conn.send(data[cnt:])
  163. if sent == 0:
  164. raise RuntimeError('connection ended')
  165. cnt += sent
  166. def read_data(conn, siz):
  167. cnt = 0
  168. buf = []
  169. while cnt < siz:
  170. data = conn.recv(min(siz - cnt, 1024))
  171. if not data:
  172. raise RuntimeError('connection ended %r %r' % (cnt, siz))
  173. buf.append(data)
  174. cnt += len(data)
  175. if sys.hexversion > 0x3000000:
  176. ret = ''.encode('iso8859-1').join(buf)
  177. else:
  178. ret = ''.join(buf)
  179. return ret
  180. def exec_command(self, cmd, **kw):
  181. if 'stdout' in kw:
  182. if kw['stdout'] not in (None, subprocess.PIPE):
  183. return self.exec_command_old(cmd, **kw)
  184. elif 'stderr' in kw:
  185. if kw['stderr'] not in (None, subprocess.PIPE):
  186. return self.exec_command_old(cmd, **kw)
  187. kw['shell'] = isinstance(cmd, str)
  188. Logs.debug('runner: %r' % cmd)
  189. Logs.debug('runner_env: kw=%s' % kw)
  190. if self.logger:
  191. self.logger.info(cmd)
  192. if 'stdout' not in kw:
  193. kw['stdout'] = subprocess.PIPE
  194. if 'stderr' not in kw:
  195. kw['stderr'] = subprocess.PIPE
  196. if Logs.verbose and not kw['shell'] and not Utils.check_exe(cmd[0]):
  197. raise Errors.WafError("Program %s not found!" % cmd[0])
  198. idx = threading.current_thread().idx
  199. kw['cmd'] = cmd
  200. # serialization..
  201. #print("sub %r %r" % (idx, cmd))
  202. #print("write to %r %r" % (idx, cmd))
  203. data = cPickle.dumps(kw, -1)
  204. params = [REQ, str(len(data))]
  205. header = make_header(params)
  206. conn = CONNS[idx]
  207. put_data(conn, header + data)
  208. #print("running %r %r" % (idx, cmd))
  209. #print("read from %r %r" % (idx, cmd))
  210. data = read_data(conn, HEADER_SIZE)
  211. if sys.hexversion > 0x3000000:
  212. data = data.decode('iso8859-1')
  213. #print("received %r" % data)
  214. lst = data.split(',')
  215. ret = int(lst[1])
  216. dlen = int(lst[2])
  217. out = err = None
  218. if dlen:
  219. data = read_data(conn, dlen)
  220. (out, err, exc) = cPickle.loads(data)
  221. if exc:
  222. raise Errors.WafError('Execution failure: %s' % exc)
  223. if out:
  224. if not isinstance(out, str):
  225. out = out.decode(sys.stdout.encoding or 'iso8859-1')
  226. if self.logger:
  227. self.logger.debug('out: %s' % out)
  228. else:
  229. Logs.info(out, extra={'stream':sys.stdout, 'c1': ''})
  230. if err:
  231. if not isinstance(err, str):
  232. err = err.decode(sys.stdout.encoding or 'iso8859-1')
  233. if self.logger:
  234. self.logger.error('err: %s' % err)
  235. else:
  236. Logs.info(err, extra={'stream':sys.stderr, 'c1': ''})
  237. return ret
  238. def init_smp(self):
  239. if not getattr(Options.options, 'smp', getattr(self, 'smp', None)):
  240. return
  241. if Utils.unversioned_sys_platform() in ('freebsd',):
  242. pid = os.getpid()
  243. cmd = ['cpuset', '-l', '0', '-p', str(pid)]
  244. elif Utils.unversioned_sys_platform() in ('linux',):
  245. pid = os.getpid()
  246. cmd = ['taskset', '-pc', '0', str(pid)]
  247. if cmd:
  248. self.cmd_and_log(cmd, quiet=0)
  249. def options(opt):
  250. # memory consumption might be at the lowest point while processing options
  251. opt.add_option('--pin-process', action='store_true', dest='smp', default=False)
  252. if Utils.is_win32 or os.sep != '/':
  253. return
  254. while len(CONNS) < 30:
  255. (pid, conn) = make_conn(opt)
  256. SERVERS.append(pid)
  257. CONNS.append(conn)
  258. def build(bld):
  259. if Utils.is_win32 or os.sep != '/':
  260. return
  261. if bld.cmd == 'clean':
  262. return
  263. while len(CONNS) < bld.jobs:
  264. (pid, conn) = make_conn(bld)
  265. SERVERS.append(pid)
  266. CONNS.append(conn)
  267. init_smp(bld)
  268. bld.__class__.exec_command_old = bld.__class__.exec_command
  269. bld.__class__.exec_command = exec_command