diff --git a/labscript/labscript.py b/labscript/labscript.py index 39fd574..887dd66 100644 --- a/labscript/labscript.py +++ b/labscript/labscript.py @@ -16,6 +16,7 @@ import sys import subprocess import keyword +import threading from inspect import getcallargs from functools import wraps @@ -31,6 +32,8 @@ import labscript_utils.h5_lock, h5py import labscript_utils.properties +from labscript_utils.labconfig import LabConfig +from labscript_utils.filewatcher import FileWatcher # This imports the default Qt library that other labscript suite code will # import as well, since it all uses qtutils. By having a Qt library already @@ -69,7 +72,10 @@ startupinfo.dwFlags |= 1 #subprocess.STARTF_USESHOWWINDOW # This variable isn't defined, but apparently it's equal to one. else: startupinfo = None - + +# Extract settings from labconfig +_SAVE_HG_INFO = LabConfig().getboolean('labscript', 'save_hg_info', fallback=True) +_SAVE_GIT_INFO = LabConfig().getboolean('labscript', 'save_git_info', fallback=False) class config(object): suppress_mild_warnings = True @@ -2173,8 +2179,90 @@ def generate_connection_table(hdf5_file): else: master_pseudoclock_name = compiler.master_pseudoclock.name dataset.attrs['master_pseudoclock'] = master_pseudoclock_name - - + +# Create a dictionary for caching results from vcs commands. The keys will be +# the paths to files that are saved during save_labscripts(). The values will be +# a list of tuples of the form (command, info, err); see the "Returns" section +# of the _run_vcs_commands() docstring for more info. Also create a FileWatcher +# instance for tracking when vcs results need updating. The callback will +# replace the outdated cache entry with a new list of updated vcs commands and +# outputs. +_vcs_cache = {} +_vcs_cache_rlock = threading.RLock() +def _file_watcher_callback(name, info, event): + with _vcs_cache_rlock: + _vcs_cache[name] = _run_vcs_commands(name) + +_file_watcher = FileWatcher(_file_watcher_callback) + +def _run_vcs_commands(path): + """Run some VCS commands on a file and return their output. + + The function is used to gather up version control system information so that + it can be stored in the hdf5 files of shots. This is for convenience and + compliments the full copy of the file already included in the shot file. + + Whether hg and git commands are run is controlled by the `save_hg_info` + and `save_git_info` options in the `[labscript]` section of the labconfig. + + Args: + path (str): The path with file name and extension of the file on which + the commands will be run. The working directory will be set to the + directory containing the specified file. + + Returns: + results (list of (tuple, str, str)): A list of tuples, each + containing information related to one vcs command of the form + (command, info, err). The first entry in that tuple is itself a + tuple of strings which was passed to subprocess.Popen() in order to + run the command. Then info is a string that contains the text + printed to stdout by that command, and err contains the text printed + to stderr by the command. + """ + # Gather together a list of commands to run. + module_directory, module_filename = os.path.split(path) + vcs_commands = [] + if compiler.save_hg_info: + hg_commands = [ + ['log', '--limit', '1'], + ['status'], + ['diff'], + ] + for command in hg_commands: + command = tuple(['hg'] + command + [module_filename]) + vcs_commands.append((command, module_directory)) + if compiler.save_git_info: + git_commands = [ + ['branch', '--show-current'], + ['describe', '--tags', '--always', 'HEAD'], + ['rev-parse', 'HEAD'], + ['diff', 'HEAD', module_filename], + ] + for command in git_commands: + command = tuple(['git'] + command) + vcs_commands.append((command, module_directory)) + + # Now go through and start running the commands. + process_list = [] + for command, module_directory in vcs_commands: + process = subprocess.Popen( + command, + cwd=module_directory, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + startupinfo=startupinfo, + ) + process_list.append((command, process)) + + # Gather up results from the commands issued. + results = [] + for command, process in process_list: + info, err = process.communicate() + info = info.decode('utf-8') + err = err.decode('utf-8') + results.append((command, info, err)) + return results + def save_labscripts(hdf5_file): if compiler.labscript_file is not None: script_text = open(compiler.labscript_file).read() @@ -2198,18 +2286,21 @@ def save_labscripts(hdf5_file): # Doesn't seem to want to double count files if you just import the contents of a file within a module continue hdf5_file.create_dataset(save_path, data=open(path).read()) - if compiler.save_hg_info: - hg_commands = [['log', '--limit', '1'], ['status'], ['diff']] - for command in hg_commands: - process = subprocess.Popen(['hg'] + command + [os.path.split(path)[1]], cwd=os.path.split(path)[0], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo) - info, err = process.communicate() - if info or err: - hdf5_file[save_path].attrs['hg ' + str(command[0])] = info.decode('utf-8') + '\n' + err.decode('utf-8') + with _vcs_cache_rlock: + already_cached = path in _vcs_cache + if not already_cached: + # Add file to watch list and create its entry in the cache. + _file_watcher.add_file(path) + _file_watcher_callback(path, None, None) + with _vcs_cache_rlock: + # Save the cached vcs output to the file. + for command, info, err in _vcs_cache[path]: + attribute_str = command[0] + ' ' + command[1] + hdf5_file[save_path].attrs[attribute_str] = (info + '\n' + err) except ImportError: pass except WindowsError if os.name == 'nt' else None: - sys.stderr.write('Warning: Cannot save Mercurial data for imported scripts. Check that the hg command can be run from the command line.\n') + sys.stderr.write('Warning: Cannot save version control data for imported scripts. Check that the hg and/or git command can be run from the command line.\n') def write_device_properties(hdf5_file): @@ -2539,7 +2630,8 @@ def labscript_cleanup(): compiler.wait_delay = 0 compiler.time_markers = {} compiler._PrimaryBLACS = None - compiler.save_hg_info = True + compiler.save_hg_info = _SAVE_HG_INFO + compiler.save_git_info = _SAVE_GIT_INFO compiler.shot_properties = {} class compiler(object): @@ -2559,7 +2651,8 @@ class compiler(object): wait_delay = 0 time_markers = {} _PrimaryBLACS = None - save_hg_info = True + save_hg_info = _SAVE_HG_INFO + save_git_info = _SAVE_GIT_INFO shot_properties = {} # safety measure in case cleanup is called before init