From 360199b84360df976ca9c9630d93a1018bb1e40d Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Fri, 5 Jun 2015 10:44:26 -0400 Subject: [PATCH] Repackage for Microdrop v1.0 --- SConstruct | 46 ------ __init__.py | 172 ++++++++++++++++++++++ hooks/Linux/on_plugin_install.sh | 3 + hooks/Windows/on_plugin_install.bat | 1 + hooks/Windows/on_plugin_install.exe | Bin 0 -> 48640 bytes microdrop/__init__.py | 172 ---------------------- on_plugin_install.py | 12 ++ release.py | 22 +++ requirements.txt | 1 + site_scons/__init__.py | 0 site_scons/git_util.py | 93 ------------ site_scons/site_tools/disttar/__init__.py | 1 - site_scons/site_tools/disttar/disttar.py | 153 ------------------- 13 files changed, 211 insertions(+), 465 deletions(-) delete mode 100644 SConstruct create mode 100644 hooks/Linux/on_plugin_install.sh create mode 100644 hooks/Windows/on_plugin_install.bat create mode 100644 hooks/Windows/on_plugin_install.exe delete mode 100644 microdrop/__init__.py create mode 100644 on_plugin_install.py create mode 100755 release.py create mode 100644 requirements.txt delete mode 100644 site_scons/__init__.py delete mode 100644 site_scons/git_util.py delete mode 100644 site_scons/site_tools/disttar/__init__.py delete mode 100644 site_scons/site_tools/disttar/disttar.py diff --git a/SConstruct b/SConstruct deleted file mode 100644 index aa3fd85..0000000 --- a/SConstruct +++ /dev/null @@ -1,46 +0,0 @@ -import re -import os -import sys - -import yaml - -from git_util import GitUtil -from path_helpers import path - - -def get_plugin_version(): - version = GitUtil(None).describe() - m = re.search('^v(?P\d+)\.(?P\d+)(-(?P\d+))?', version) - if m.group('micro'): - micro = m.group('micro') - else: - micro = '0' - version_string = "%s.%s.%s" % (m.group('major'), - m.group('minor'), micro) - return version_string - - -SOFTWARE_VERSION = get_plugin_version() -env = Environment(tools = ["default", "disttar"], - DISTTAR_EXCLUDEDIRS=['.git'], - DISTTAR_EXCLUDERES=[r'\.sconsign\.dblite'], - DISTTAR_EXCLUDEEXTS=['.gz', '.pyc', '.tgz', '.swp']) - -plugin_root = path('.').abspath() -properties_target = plugin_root.joinpath('properties.yml') -properties = {'plugin_name': 'wheeler.%s' % plugin_root.name, - 'package_name': str(plugin_root.name), - 'version': SOFTWARE_VERSION} -properties_target.write_bytes(yaml.dump(properties)) -archive_name = '%s-%s.tar.gz' % (properties['package_name'], SOFTWARE_VERSION) - -# This will build an archive using what ever DISTTAR_FORMAT that is set. -tar = env.DistTar('%s' % properties['package_name'], [env.Dir('#')]) -renamed_tar = env.Command(env.File(archive_name), None, - Move(archive_name, tar[0])) -Depends(renamed_tar, tar) -Clean(renamed_tar, tar) - -if 'PLUGIN_ARCHIVE_DIR' in os.environ: - target_archive_dir = os.environ['PLUGIN_ARCHIVE_DIR'] - Install(target_archive_dir, renamed_tar) diff --git a/__init__.py b/__init__.py index e69de29..9c97ea6 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1,172 @@ +""" +Copyright 2011 Ryan Fobel + +This file is part of dmf_control_board. + +dmf_control_board is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +dmf_control_board is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with dmf_control_board. If not, see . +""" +from collections import OrderedDict +from datetime import datetime + +import gtk +import zmq +from flatland import String, Boolean, Float, Form +from logger import logger +from gui.protocol_grid_controller import ProtocolGridController +from plugin_helpers import (AppDataController, StepOptionsController, + get_plugin_info) +from plugin_manager import (IPlugin, IWaveformGenerator, Plugin, implements, + PluginGlobals, ScheduleRequest, emit_signal, + get_service_instance) +from app_context import get_app +from path_helpers import path + + +PluginGlobals.push_env('microdrop.managed') + +class ZeroMQServicePlugin(Plugin, AppDataController, StepOptionsController): + """ + This class is automatically registered with the PluginManager. + """ + implements(IPlugin) + version = get_plugin_info(path(__file__).parent.parent).version + plugins_name = get_plugin_info(path(__file__).parent.parent).plugin_name + + ''' + AppFields + --------- + + A flatland Form specifying application options for the current plugin. + Note that nested Form objects are not supported. + + Since we subclassed AppDataController, an API is available to access and + modify these attributes. This API also provides some nice features + automatically: + -all fields listed here will be included in the app options dialog + (unless properties=dict(show_in_gui=False) is used) + -the values of these fields will be stored persistently in the microdrop + config file, in a section named after this plugin's name attribute + ''' + AppFields = Form.of( + String.named('service_address').using(default='', optional=True), + ) + + ''' + StepFields + --------- + + A flatland Form specifying the per step options for the current plugin. + Note that nested Form objects are not supported. + + Since we subclassed StepOptionsController, an API is available to access and + modify these attributes. This API also provides some nice features + automatically: + -all fields listed here will be included in the protocol grid view + (unless properties=dict(show_in_gui=False) is used) + -the values of these fields will be stored persistently for each step + ''' + StepFields = Form.of( + Boolean.named('service_enabled').using(default=False, optional=True), + ) + + def __init__(self): + self.name = self.plugins_name + self.context = zmq.Context.instance() + self.socks = OrderedDict() + self.timeout_id = None + self._start_time = None + + def on_plugin_enable(self): + # We need to call AppDataController's on_plugin_enable() to update the + # application options data. + AppDataController.on_plugin_enable(self) + self.context = zmq.Context() + self.reset_socks() + if get_app().protocol: + pgc = get_service_instance(ProtocolGridController, env='microdrop') + pgc.update_grid() + + def close_socks(self): + # Close any currently open sockets. + for name, sock in self.socks.iteritems(): + sock.close() + self.socks = OrderedDict() + + def reset_socks(self): + self.close_socks() + app_values = self.get_app_values() + if self.timeout_id is not None: + gtk.timeout_remove(self.timeout_id) + self.timeout_id = None + if app_values['service_address']: + # Service address is available + self.socks['req'] = zmq.Socket(self.context, zmq.REQ) + self.socks['req'].connect(app_values['service_address']) + + def on_app_options_changed(self, plugin_name): + if plugin_name == self.name: + self.reset_socks() + + def on_plugin_disable(self): + self.close_socks() + if get_app().protocol: + pgc = get_service_instance(ProtocolGridController, env='microdrop') + pgc.update_grid() + + def step_complete(self, return_value=None): + app = get_app() + if app.running or app.realtime_mode: + emit_signal('on_step_complete', [self.name, return_value]) + + def on_step_run(self): + options = self.get_step_options() + self.reset_socks() + if options['service_enabled'] and self.socks['req'] is None: + # Service is supposed to be called for this step, but the socket is + # not ready. + self.step_complete(return_value='Fail') + elif options['service_enabled'] and self.socks['req'] is not None: + logger.info('[ZeroMQServicePlugin] Send signal to service to ' + 'start.') + # Request start of service. + self.socks['req'].send('start') + if not self.socks['req'].poll(timeout=4000): + self.reset_socks() + logger.error('[ZeroMQServicePlugin] Timed-out waiting for ' + 'a response.') + else: + # Response is ready. + response = self.socks['req'].recv() + if response == 'started': + logger.info('[ZeroMQServicePlugin] Service started ' + 'successfully.') + self.socks['req'].send('notify_completion') + + response = self.socks['req'].recv() + logger.info('[ZeroMQServicePlugin] Service response: %s', response) + if response == 'completed': + logger.info('[ZeroMQServicePlugin] Service completed task ' + 'successfully.') + self.step_complete() + else: + logger.error('[ZeroMQServicePlugin] Unexpected response: %s' % + response) + self.step_complete(return_value='Fail') + else: + self.step_complete() + + def enable_service(self): + pass + +PluginGlobals.pop_env() diff --git a/hooks/Linux/on_plugin_install.sh b/hooks/Linux/on_plugin_install.sh new file mode 100644 index 0000000..19bf666 --- /dev/null +++ b/hooks/Linux/on_plugin_install.sh @@ -0,0 +1,3 @@ +#!/bin/sh +PYTHON_EXE=$1 +$PYTHON_EXE ../../on_plugin_install.py diff --git a/hooks/Windows/on_plugin_install.bat b/hooks/Windows/on_plugin_install.bat new file mode 100644 index 0000000..617a828 --- /dev/null +++ b/hooks/Windows/on_plugin_install.bat @@ -0,0 +1 @@ +%1 ..\..\on_plugin_install.py diff --git a/hooks/Windows/on_plugin_install.exe b/hooks/Windows/on_plugin_install.exe new file mode 100644 index 0000000000000000000000000000000000000000..2e6aa4f40e0ba6fabfae628dd3976df1f76b4eb7 GIT binary patch literal 48640 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 literal 0 HcmV?d00001 diff --git a/microdrop/__init__.py b/microdrop/__init__.py deleted file mode 100644 index 9c97ea6..0000000 --- a/microdrop/__init__.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -Copyright 2011 Ryan Fobel - -This file is part of dmf_control_board. - -dmf_control_board is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -dmf_control_board is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with dmf_control_board. If not, see . -""" -from collections import OrderedDict -from datetime import datetime - -import gtk -import zmq -from flatland import String, Boolean, Float, Form -from logger import logger -from gui.protocol_grid_controller import ProtocolGridController -from plugin_helpers import (AppDataController, StepOptionsController, - get_plugin_info) -from plugin_manager import (IPlugin, IWaveformGenerator, Plugin, implements, - PluginGlobals, ScheduleRequest, emit_signal, - get_service_instance) -from app_context import get_app -from path_helpers import path - - -PluginGlobals.push_env('microdrop.managed') - -class ZeroMQServicePlugin(Plugin, AppDataController, StepOptionsController): - """ - This class is automatically registered with the PluginManager. - """ - implements(IPlugin) - version = get_plugin_info(path(__file__).parent.parent).version - plugins_name = get_plugin_info(path(__file__).parent.parent).plugin_name - - ''' - AppFields - --------- - - A flatland Form specifying application options for the current plugin. - Note that nested Form objects are not supported. - - Since we subclassed AppDataController, an API is available to access and - modify these attributes. This API also provides some nice features - automatically: - -all fields listed here will be included in the app options dialog - (unless properties=dict(show_in_gui=False) is used) - -the values of these fields will be stored persistently in the microdrop - config file, in a section named after this plugin's name attribute - ''' - AppFields = Form.of( - String.named('service_address').using(default='', optional=True), - ) - - ''' - StepFields - --------- - - A flatland Form specifying the per step options for the current plugin. - Note that nested Form objects are not supported. - - Since we subclassed StepOptionsController, an API is available to access and - modify these attributes. This API also provides some nice features - automatically: - -all fields listed here will be included in the protocol grid view - (unless properties=dict(show_in_gui=False) is used) - -the values of these fields will be stored persistently for each step - ''' - StepFields = Form.of( - Boolean.named('service_enabled').using(default=False, optional=True), - ) - - def __init__(self): - self.name = self.plugins_name - self.context = zmq.Context.instance() - self.socks = OrderedDict() - self.timeout_id = None - self._start_time = None - - def on_plugin_enable(self): - # We need to call AppDataController's on_plugin_enable() to update the - # application options data. - AppDataController.on_plugin_enable(self) - self.context = zmq.Context() - self.reset_socks() - if get_app().protocol: - pgc = get_service_instance(ProtocolGridController, env='microdrop') - pgc.update_grid() - - def close_socks(self): - # Close any currently open sockets. - for name, sock in self.socks.iteritems(): - sock.close() - self.socks = OrderedDict() - - def reset_socks(self): - self.close_socks() - app_values = self.get_app_values() - if self.timeout_id is not None: - gtk.timeout_remove(self.timeout_id) - self.timeout_id = None - if app_values['service_address']: - # Service address is available - self.socks['req'] = zmq.Socket(self.context, zmq.REQ) - self.socks['req'].connect(app_values['service_address']) - - def on_app_options_changed(self, plugin_name): - if plugin_name == self.name: - self.reset_socks() - - def on_plugin_disable(self): - self.close_socks() - if get_app().protocol: - pgc = get_service_instance(ProtocolGridController, env='microdrop') - pgc.update_grid() - - def step_complete(self, return_value=None): - app = get_app() - if app.running or app.realtime_mode: - emit_signal('on_step_complete', [self.name, return_value]) - - def on_step_run(self): - options = self.get_step_options() - self.reset_socks() - if options['service_enabled'] and self.socks['req'] is None: - # Service is supposed to be called for this step, but the socket is - # not ready. - self.step_complete(return_value='Fail') - elif options['service_enabled'] and self.socks['req'] is not None: - logger.info('[ZeroMQServicePlugin] Send signal to service to ' - 'start.') - # Request start of service. - self.socks['req'].send('start') - if not self.socks['req'].poll(timeout=4000): - self.reset_socks() - logger.error('[ZeroMQServicePlugin] Timed-out waiting for ' - 'a response.') - else: - # Response is ready. - response = self.socks['req'].recv() - if response == 'started': - logger.info('[ZeroMQServicePlugin] Service started ' - 'successfully.') - self.socks['req'].send('notify_completion') - - response = self.socks['req'].recv() - logger.info('[ZeroMQServicePlugin] Service response: %s', response) - if response == 'completed': - logger.info('[ZeroMQServicePlugin] Service completed task ' - 'successfully.') - self.step_complete() - else: - logger.error('[ZeroMQServicePlugin] Unexpected response: %s' % - response) - self.step_complete(return_value='Fail') - else: - self.step_complete() - - def enable_service(self): - pass - -PluginGlobals.pop_env() diff --git a/on_plugin_install.py b/on_plugin_install.py new file mode 100644 index 0000000..80fb487 --- /dev/null +++ b/on_plugin_install.py @@ -0,0 +1,12 @@ +from datetime import datetime +import logging + +from path_helpers import path +from pip_helpers import install + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + logging.info(str(datetime.now())) + requirements = path(__file__).parent.joinpath('requirements.txt').abspath() + logging.info(install(['-U', '-r', requirements])) diff --git a/release.py b/release.py new file mode 100755 index 0000000..47788c4 --- /dev/null +++ b/release.py @@ -0,0 +1,22 @@ +import tarfile +import yaml + +from microdrop_utility import Version + +package_name = 'dstat_zeromq_plugin' +plugin_name = 'wheeler.dstat_zeromq_plugin' + +# create a version sting based on the git revision/branch +version = str(Version.from_git_repository()) + +# write the 'properties.yml' file +properties = {'plugin_name': plugin_name, 'package_name': package_name, + 'version': version} +with open('properties.yml', 'w') as f: + f.write(yaml.dump(properties)) + +# create the tar.gz plugin archive +with tarfile.open("%s-%s.tar.gz" % (package_name, version), "w:gz") as tar: + for name in ['__init__.py', 'test_service.py', 'properties.yml', + 'requirements.txt']: + tar.add(name) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..02ec117 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyzmq diff --git a/site_scons/__init__.py b/site_scons/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/site_scons/git_util.py b/site_scons/git_util.py deleted file mode 100644 index 6fecf25..0000000 --- a/site_scons/git_util.py +++ /dev/null @@ -1,93 +0,0 @@ -import os -from subprocess import Popen, PIPE, check_call, CalledProcessError -import re - -from path_helpers import path - - -class GitError(Exception): - pass - - -class GitUtil(object): - def __init__(self, root_path='.'): - self.root_path = path(root_path) - if root_path is None: - dir_node = path(os.getcwd()) - while not dir_node.dirs('.git') and dir_node: - dir_node = dir_node.parent - if not dir_node: - raise GitError('No git root found.') - self.root_path = dir_node - self._git = None - assert(self.root_path.dirs('.git')) - - - @property - def git(self): - if self._git: - return self._git - - cmds = ['git'] - if os.name == 'nt': - exceptions = (WindowsError,) - cmds += ['git.cmd'] - else: - exceptions = (OSError,) - - valid_cmd = False - for cmd in cmds: - try: - check_call([cmd], stdout=PIPE, stderr=PIPE) - except exceptions: - # The command was not found, try the next one - pass - except CalledProcessError: - valid_cmd = True - break - if not valid_cmd: - raise GitError, 'No valid git command found' - self._git = cmd - return self._git - - - def command(self, x): - try: - x.__iter__ - except: - x = re.split(r'\s+', x) - cwd = os.getcwd() - - os.chdir(self.root_path) - cmd = [self.git] + x - stdout, stderr = Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE).communicate() - os.chdir(cwd) - - if stderr: - raise GitError('Error executing git %s' % x) - return stdout.strip() - - - def describe(self): - return self.command('describe') - - - def summary(self, color=False): - if color: - format_ = '''--pretty=format:%Cred%h%Creset - %s %Cgreen(%cr)%Creset''' - else: - format_ = '''--pretty=format:%h - %s (%cr)''' - return self.command(['''log''', '''--graph''', format_, - '''--abbrev-commit''', '''--date=relative''']) - - - def rev_parse(self, ref='HEAD'): - return self.command(['rev-parse', ref]) - - - def show(self, ref='HEAD', color=False, extra_args=None): - extra_args = [extra_args, []][extra_args is None] - args = ['show', ref] - if color: - args += ['--color'] - return self.command(args + extra_args) diff --git a/site_scons/site_tools/disttar/__init__.py b/site_scons/site_tools/disttar/__init__.py deleted file mode 100644 index 4b0e0f0..0000000 --- a/site_scons/site_tools/disttar/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from disttar import * diff --git a/site_scons/site_tools/disttar/disttar.py b/site_scons/site_tools/disttar/disttar.py deleted file mode 100644 index 7f7a13e..0000000 --- a/site_scons/site_tools/disttar/disttar.py +++ /dev/null @@ -1,153 +0,0 @@ -# DistTarBuilder: tool to generate tar files using SCons -# Copyright (C) 2005, 2006 Matthew A. Nicholson -# Copyright (C) 2006-2010 John Pye -# -# This file is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License version 2.1 as published by the Free Software Foundation. -# -# This file is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -import os,sys -from SCons.Script import * -import re - - -def file_handler(fpath, source, excluderes, excludeexts): - from path import path - - fpath = path(fpath) - if not fpath.ext in excludeexts: - failre = False - for r in excluderes: - #print "Match( %s against %s)" % (r,relpath) - if r.search(fpath): - failre = True - #print "Excluding '%s' from tarball" % fpath - break - if not failre: - #print "Adding source",fpath - source.append(str(fpath)) - - -def disttar_emitter(target, source, env): - source,origsource = [], source - - excludeexts = env.Dictionary().get('DISTTAR_EXCLUDEEXTS',[]) - excludedirs = env.Dictionary().get('DISTTAR_EXCLUDEDIRS',[]) - re1 = env.Dictionary().get('DISTTAR_EXCLUDERES',[]) - excluderes = [re.compile(r) for r in re1] - - # assume the sources are directories... need to check that - for item in origsource: - if os.path.isfile(str(item)): - file_handler(item, source, excluderes, excludeexts) - else: - for root, dirs, files in os.walk(str(item)): - # don't make directory dependences as that triggers full build - # of that directory - if root in source: - #print "Removing directory %s" % root - source.remove(root) - - # loop through files in a directory - for name in files: - relpath = os.path.join(root,name) - file_handler(relpath, source, excluderes, excludeexts) - - for d in excludedirs: - if d in dirs: - dirs.remove(d) # don't visit CVS directories etc - - return target, source - -def disttar_string(target, source, env): - """This is what gets printed on the console. We'll strip out the list - or source files, since it tends to get very long. If you want to see the - contents, the easiest way is to uncomment the line 'Adding to TAR file' - below. """ - return 'DistTar(%s,...)' % target[0] - -def disttar(target, source, env): - """tar archive builder""" - - import tarfile - - env_dict = env.Dictionary() - - if env_dict.get("DISTTAR_FORMAT") in ["gz", "bz2"]: - tar_format = env_dict["DISTTAR_FORMAT"] - else: - tar_format = "" - - # split the target directory, filename, and stuffix - base_name = str(target[0]).split('.tar')[0] - (target_dir, dir_name) = os.path.split(base_name) - - # create the target directory if it does not exist - if target_dir and not os.path.exists(target_dir): - os.makedirs(target_dir) - - # open our tar file for writing - print >> sys.stderr, 'DistTar: Writing %s' % str(target[0]) - print >> sys.stderr, ' with contents: %s' % [str(s) for s in source] - tar = tarfile.open(str(target[0]), "w:%s" % tar_format) - - # write sources to our tar file - for item in source: - item = str(item) - sys.stderr.write(".") - #print "Adding to TAR file: %s/%s" % (dir_name,item) - tar.add(item,'%s/%s' % (dir_name,item)) - - # all done - sys.stderr.write("\n") #print "Closing TAR file" - tar.close() - -def disttar_suffix(env, sources): - """tar archive suffix generator""" - - env_dict = env.Dictionary() - if env_dict.has_key("DISTTAR_FORMAT") and env_dict["DISTTAR_FORMAT"] in ["gz", "bz2"]: - return ".tar." + env_dict["DISTTAR_FORMAT"] - else: - return ".tar" - -def generate(env): - """ - Add builders and construction variables for the DistTar builder. - """ - - disttar_action=SCons.Action.Action(disttar, disttar_string) - env['BUILDERS']['DistTar'] = Builder( - action=disttar_action - , emitter=disttar_emitter - , suffix = disttar_suffix - , target_factory = env.fs.Entry - ) - - env.AppendUnique( - DISTTAR_FORMAT = 'gz' - ) - -def exists(env): - """ - Make sure this tool exists. - """ - try: - import os - import tarfile - except ImportError: - return False - else: - return True - -# vim:set ts=4 sw=4 noexpandtab: -- GitLab