PageRenderTime 71ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/ybd/app.py

https://gitlab.com/baserock/ybd
Python | 320 lines | 268 code | 28 blank | 24 comment | 35 complexity | 28419faab093785ae534bb6eaaa4dd51 MD5 | raw file
  1. # Copyright (C) 2014-2016 Codethink Limited
  2. #
  3. # This program is free software; you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation; version 2 of the License.
  6. #
  7. # This program is distributed in the hope that it will be useful,
  8. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. # GNU General Public License for more details.
  11. #
  12. # You should have received a copy of the GNU General Public License along
  13. # with this program. If not, see <http://www.gnu.org/licenses/>.
  14. #
  15. # =*= License: GPL-2 =*=
  16. import contextlib
  17. import datetime
  18. import os
  19. import fcntl
  20. import re
  21. import shutil
  22. import sys
  23. import warnings
  24. import yaml
  25. from multiprocessing import cpu_count, Value, Lock
  26. from subprocess import call
  27. from fs.osfs import OSFS # not used here, but we import it to check install
  28. from repos import get_version
  29. from cache import cache_key
  30. config = {}
  31. defs = {}
  32. class RetryException(Exception):
  33. def __init__(self, dn):
  34. if config.get('last-retry-dn') != dn:
  35. log(dn, 'Already assembling, so wait/retry', verbose=True)
  36. if config.get('last-retry-time'):
  37. wait = datetime.datetime.now() - config.get('last-retry-time')
  38. if wait.seconds < 1 and os.path.exists(lockfile(dn)):
  39. try:
  40. with open(lockfile(dn), 'r') as L:
  41. call(['flock', '--shared', '--timeout',
  42. config.get('timeout', '60'), str(L.fileno())])
  43. log(dn, 'Finished wait loop', verbose=True)
  44. except IOError:
  45. # The process that had the lock is finished with it.
  46. pass
  47. config['last-retry-time'] = datetime.datetime.now()
  48. config['last-retry-dn'] = dn
  49. for dirname in config['sandboxes']:
  50. remove_dir(dirname)
  51. config['sandboxes'] = []
  52. # Code taken from Eli Bendersky's example at
  53. # http://eli.thegreenplace.net/2012/01/04/shared-counter-with-pythons-multiprocessing
  54. class Counter(object):
  55. def __init__(self, initval=0):
  56. self.val = Value('i', initval)
  57. self.lock = Lock()
  58. def increment(self):
  59. with self.lock:
  60. self.val.value += 1
  61. def get(self):
  62. with self.lock:
  63. return self.val.value
  64. def lockfile(dn):
  65. return os.path.join(config['tmp'], cache_key(dn) + '.lock')
  66. def log(dn, message='', data='', verbose=False, exit=False):
  67. ''' Print a timestamped log. '''
  68. if exit:
  69. print('\n\n')
  70. message = 'ERROR: ' + message.replace('WARNING: ', '')
  71. if verbose is True and config.get('log-verbose', False) is False:
  72. return
  73. name = dn['name'] if type(dn) is dict else dn
  74. timestamp = datetime.datetime.now().strftime('%y-%m-%d %H:%M:%S ')
  75. if config.get('log-timings') == 'elapsed':
  76. timestamp = timestamp[:9] + elapsed(config['start-time']) + ' '
  77. if config.get('log-timings', 'omit') == 'omit':
  78. timestamp = ''
  79. progress = ''
  80. if config.get('counter'):
  81. count = config['counter'].get()
  82. progress = '[%s/%s/%s] ' % (count, config['tasks'], config['total'])
  83. entry = '%s%s[%s] %s %s\n' % (timestamp, progress, name, message, data)
  84. if config.get('instances'):
  85. entry = str(config.get('fork', 0)) + ' ' + entry
  86. print(entry),
  87. sys.stdout.flush()
  88. if exit:
  89. print('\n\n')
  90. os._exit(1)
  91. def log_env(log, env, message=''):
  92. with open(log, "a") as logfile:
  93. for key in sorted(env):
  94. msg = env[key] if 'PASSWORD' not in key else '(hidden)'
  95. if 'URL' in key.upper():
  96. msg = re.sub(r'(https://)[^@]*@', '\\1', env[key])
  97. logfile.write('%s=%s\n' % (key, msg))
  98. logfile.write(message + '\n\n')
  99. logfile.flush()
  100. def warning_handler(message, category, filename, lineno, file=None, line=None):
  101. '''Output messages from warnings.warn() - default output is a bit ugly.'''
  102. return 'WARNING: %s\n' % (message)
  103. def setup(program, target, arch, mode, original_cwd=""):
  104. os.environ['LANG'] = 'en_US.UTF-8'
  105. # Installation of git-lfs on a system can pollute /etc/gitconfig. Setting
  106. # GIT_CONFIG_NOSYSTEM so ybd will ignore the system config.
  107. os.environ['GIT_CONFIG_NOSYSTEM'] = "1"
  108. config['start-time'] = datetime.datetime.now()
  109. config['program'] = os.path.basename(program)
  110. config['my-version'] = get_version(os.path.dirname(__file__))
  111. log('SETUP', '%s version is' % config['program'], config['my-version'])
  112. log('SETUP', 'Running %s in' % program, os.getcwd())
  113. config['filename'] = os.path.basename(target)
  114. config['target'] = re.sub('.morph$', '', config['filename'])
  115. config['arch'] = arch
  116. config['sandboxes'] = []
  117. config['overlaps'] = []
  118. config['new-overlaps'] = []
  119. warnings.formatwarning = warning_handler
  120. # Suppress multiple instances of the same warning.
  121. warnings.simplefilter('once', append=True)
  122. # dump any applicable environment variables into a config file
  123. with open('./ybd.environment', 'w') as f:
  124. for key in os.environ:
  125. if key[:4] == "YBD_":
  126. f.write(key[4:] + ": " + os.environ.get(key) + '\n')
  127. # load config files in reverse order of precedence
  128. load_configs([
  129. os.path.join(os.getcwd(), 'ybd.environment'),
  130. os.path.join(os.getcwd(), 'ybd.conf'),
  131. os.path.join(original_cwd, 'ybd.conf'),
  132. os.path.join(os.path.dirname(__file__), '..', 'ybd.conf'),
  133. os.path.join(os.path.dirname(__file__), 'config', 'ybd.conf')])
  134. # After loading configuration, override the 'mode' configuration only
  135. # if it was specified on the command line
  136. if mode is not None:
  137. config['mode'] = mode
  138. if not os.geteuid() == 0 and config.get('mode') == 'normal':
  139. log('SETUP', '%s needs root permissions' % program, exit=True)
  140. if config.get('kbas-url', 'http://foo.bar/') == 'http://foo.bar/':
  141. config.pop('kbas-url')
  142. if config.get('kbas-url'):
  143. if not config['kbas-url'].endswith('/'):
  144. config['kbas-url'] += '/'
  145. config['total'] = config['tasks'] = config['counter'] = 0
  146. config['systems'] = config['strata'] = config['chunks'] = 0
  147. config['reproduced'] = []
  148. config['keys'] = []
  149. config['pid'] = os.getpid()
  150. config['def-version'] = get_version('.')
  151. config['defdir'] = os.getcwd()
  152. config['extsdir'] = os.path.join(config['defdir'], 'extensions')
  153. if config.get('manifest') is True:
  154. config['manifest'] = os.path.join(config['defdir'],
  155. os.path.basename(config['target']) +
  156. '.manifest')
  157. try:
  158. os.remove(config['manifest'])
  159. except OSError:
  160. pass
  161. base_dir = os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~')
  162. config.setdefault('base',
  163. os.path.join(base_dir, config['directories']['base']))
  164. for directory, path in config.get('directories', {}).items():
  165. try:
  166. if config.get(directory) is None:
  167. if path is None:
  168. path = os.path.join(config.get('base', '/'), directory)
  169. config[directory] = path
  170. os.makedirs(config[directory])
  171. except OSError:
  172. if not os.path.isdir(config[directory]):
  173. log('SETUP', 'Cannot find or create', config[directory],
  174. exit=True)
  175. log('SETUP', '%s is directory for' % config[directory], directory)
  176. # git replace means we can't trust that just the sha1 of a branch
  177. # is enough to say what it contains, so we turn it off by setting
  178. # the right flag in an environment variable.
  179. os.environ['GIT_NO_REPLACE_OBJECTS'] = '1'
  180. if 'max-jobs' not in config:
  181. config['max-jobs'] = cpu_count()
  182. if 'instances' not in config:
  183. # based on some testing (mainly on AWS), maximum effective
  184. # max-jobs value seems to be around 8-10 if we have enough cores
  185. # users should set values based on workload and build infrastructure
  186. # FIXME: more testing :)
  187. if cpu_count() >= 10:
  188. config['instances'] = 1 + (cpu_count() / 10)
  189. config['max-jobs'] = cpu_count() / config['instances']
  190. config['pid'] = os.getpid()
  191. config['counter'] = Counter()
  192. log('SETUP', 'Max-jobs is set to', config['max-jobs'])
  193. def load_configs(config_files):
  194. for config_file in reversed(config_files):
  195. if os.path.exists(config_file):
  196. with open(config_file) as f:
  197. text = f.read()
  198. if yaml.safe_load(text) is None:
  199. return
  200. log('SETUP', 'Setting config from %s:' % config_file)
  201. for key, value in yaml.safe_load(text).items():
  202. config[key.replace('_', '-')] = value
  203. msg = value if 'PASSWORD' not in key.upper() else '(hidden)'
  204. if 'URL' in key.upper():
  205. msg = re.sub(r'(https://)[^@]*@', '\\1', str(value))
  206. print ' %s=%s' % (key.replace('_', '-'), msg)
  207. print
  208. def cleanup(tmpdir):
  209. if not config.get('cleanup', True):
  210. log('SETUP', 'WARNING: no cleanup for', tmpdir)
  211. return
  212. try:
  213. log('SETUP', 'Trying cleanup for', tmpdir)
  214. with open(os.path.join(tmpdir, 'lock'), 'w') as tmp_lock:
  215. fcntl.flock(tmp_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
  216. to_delete = os.listdir(tmpdir)
  217. fcntl.flock(tmp_lock, fcntl.LOCK_SH | fcntl.LOCK_NB)
  218. if os.fork() == 0:
  219. for dirname in to_delete:
  220. remove_dir(os.path.join(tmpdir, dirname))
  221. log('SETUP', 'Cleanup successful for', tmpdir)
  222. sys.exit(0)
  223. except IOError:
  224. log('SETUP', 'WARNING: no cleanup for', tmpdir)
  225. def remove_dir(tmpdir):
  226. if (os.path.dirname(tmpdir) == config['tmp']) and os.path.isdir(tmpdir):
  227. try:
  228. shutil.rmtree(tmpdir)
  229. except:
  230. log('SETUP', 'WARNING: unable to remove', tmpdir)
  231. @contextlib.contextmanager
  232. def chdir(dirname=None):
  233. currentdir = os.getcwd()
  234. try:
  235. if dirname is not None:
  236. os.chdir(dirname)
  237. yield
  238. finally:
  239. os.chdir(currentdir)
  240. @contextlib.contextmanager
  241. def timer(dn, message=''):
  242. starttime = datetime.datetime.now()
  243. log(dn, 'Starting ' + message)
  244. if type(dn) is dict:
  245. dn['start-time'] = starttime
  246. try:
  247. yield
  248. except:
  249. raise
  250. text = '' if message == '' else ' for ' + message
  251. time_elapsed = elapsed(starttime)
  252. log(dn, 'Elapsed time' + text, time_elapsed)
  253. def elapsed(starttime):
  254. td = datetime.datetime.now() - starttime
  255. hours, remainder = divmod(int(td.total_seconds()), 60*60)
  256. minutes, seconds = divmod(remainder, 60)
  257. return "%02d:%02d:%02d" % (hours, minutes, seconds)
  258. def spawn():
  259. for fork in range(1, config.get('instances')):
  260. if os.fork() == 0:
  261. config['fork'] = fork
  262. log('FORKS', 'I am fork', config.get('fork'))
  263. break