artiq/artiq/master/repository.py

149 lines
4.3 KiB
Python

import asyncio
import os
import tempfile
import shutil
import logging
from artiq.protocols.sync_struct import Notifier
from artiq.master.worker import Worker
from artiq.tools import exc_to_warning
logger = logging.getLogger(__name__)
async def _scan_experiments(wd, get_device_db, log):
r = dict()
for f in os.listdir(wd):
if f.endswith(".py"):
try:
worker = Worker({
"get_device_db": get_device_db,
"log": lambda message: log("scan", message)
})
try:
description = await worker.examine(os.path.join(wd, f))
finally:
await worker.close()
for class_name, class_desc in description.items():
name = class_desc["name"]
arguments = class_desc["arguments"]
if name in r:
logger.warning("Duplicate experiment name: '%s'", name)
basename = name
i = 1
while name in r:
name = basename + str(i)
i += 1
entry = {
"file": f,
"class_name": class_name,
"arguments": arguments
}
r[name] = entry
except:
logger.warning("Skipping file '%s'", f, exc_info=True)
return r
def _sync_explist(target, source):
for k in list(target.read.keys()):
if k not in source:
del target[k]
for k in source.keys():
if k not in target.read or target.read[k] != source[k]:
target[k] = source[k]
class Repository:
def __init__(self, backend, get_device_db_fn, log_fn):
self.backend = backend
self.get_device_db_fn = get_device_db_fn
self.log_fn = log_fn
self.cur_rev = self.backend.get_head_rev()
self.backend.request_rev(self.cur_rev)
self.explist = Notifier(dict())
self._scanning = False
def close(self):
# The object cannot be used anymore after calling this method.
self.backend.release_rev(self.cur_rev)
async def scan(self, new_cur_rev=None):
if self._scanning:
return
self._scanning = True
try:
if new_cur_rev is None:
new_cur_rev = self.backend.get_head_rev()
wd, _ = self.backend.request_rev(new_cur_rev)
self.backend.release_rev(self.cur_rev)
self.cur_rev = new_cur_rev
new_explist = await _scan_experiments(wd, self.get_device_db_fn,
self.log_fn)
_sync_explist(self.explist, new_explist)
finally:
self._scanning = False
def scan_async(self, new_cur_rev=None):
asyncio.ensure_future(exc_to_warning(self.scan(new_cur_rev)))
class FilesystemBackend:
def __init__(self, root):
self.root = os.path.abspath(root)
def get_head_rev(self):
return "N/A"
def request_rev(self, rev):
return self.root, None
def release_rev(self, rev):
pass
class _GitCheckout:
def __init__(self, git, rev):
self.path = tempfile.mkdtemp()
commit = git.get(rev)
git.checkout_tree(commit, directory=self.path)
self.message = commit.message.strip()
self.ref_count = 1
logger.info("checked out revision %s into %s", rev, self.path)
def dispose(self):
logger.info("disposing of checkout in folder %s", self.path)
shutil.rmtree(self.path)
class GitBackend:
def __init__(self, root):
# lazy import - make dependency optional
import pygit2
self.git = pygit2.Repository(root)
self.checkouts = dict()
def get_head_rev(self):
return str(self.git.head.target)
def request_rev(self, rev):
if rev in self.checkouts:
co = self.checkouts[rev]
co.ref_count += 1
else:
co = _GitCheckout(self.git, rev)
self.checkouts[rev] = co
return co.path, co.message
def release_rev(self, rev):
co = self.checkouts[rev]
co.ref_count -= 1
if not co.ref_count:
co.dispose()
del self.checkouts[rev]