Commits

Josh VanderLinden committed 3d767a4

Major refactoring business again... I started work on a Windows service. Implemented a ConfigParser setup.

Comments (0)

Files changed (14)

 setup.py
 clip2zeus/__init__.py
 clip2zeus/clip2zeus.py
+clip2zeus/clip2zeus_service.py
 clip2zeus/clip2zeus_ctl.py
 clip2zeus/common.py
+clip2zeus/global.py
 clip2zeus/gui.py
 clip2zeus/linux.py
 clip2zeus/osx.py

clip2zeus/__init__.py

-from .clip2zeus import main, APP_TITLE, __version__, __author__
+from clip2zeus.main import main
+from clip2zeus.common import *
+from clip2zeus.common import __version__, __author__
 

clip2zeus/clip2zeus.py

-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Monitors the system clipboard for text that contains URLs for conversion
-using 2ze.us
-"""
-
-from common import *
-
-def main():
-    from optparse import OptionParser
-
-    parser = OptionParser()
-    parser.add_option('-p', '--port', dest='port', default=DEFAULT_PORT, help='The port for the daemon to listen on')
-    options, args = parser.parse_args()
-
-    params = dict(
-        port=options.port,
-    )
-
-    clip2zeus = Clip2ZeusApp.for_platform()
-    clip2zeus(**params).start()
-
-if __name__ == '__main__':
-    main()
-

clip2zeus/clip2zeus_ctl.py

 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
-from common import Clip2ZeusApp, Clip2ZeusCtl, DEFAULT_PORT
+from clip2zeus.common import Clip2ZeusApp, Clip2ZeusCtl, DEFAULT_PORT
 
 def main():
     from optparse import OptionParser

clip2zeus/clip2zeus_service.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+A Windows service for running the Clip2Zeus application in the background
+on Windows.
+"""
+
+from win32serviceutil import ServiceFramework, HandleCommandLine
+import servicemanager
+import win32service
+
+class Clip2ZeusService(ServiceFramework):
+
+    _svc_name_ = "Clip2Zeus Service"
+    _svc_display_name_ = "Clip2Zeus Automatic URL Shortening"
+
+    def __init__(self, args):
+        super(Clip2ZeusService, self).__init__(args)
+        self.isAlive = True
+        self.clip2zeus = None
+
+    def SvcDoRun(self):
+        """Starts the service"""
+
+        from clip2zeus.win32 import Clip2ZeusWin32
+
+        servicemanager.LogInfoMsg("Starting %s" % Clip2ZeusWin32._svc_name_)
+        self.clip2zeus = Clip2ZeusWin32()
+        self.clip2zeus.start()
+
+    def SvcStop(self):
+        """Stops the service"""
+
+        servicemanager.LogInfoMsg("Stopping %s" % Clip2ZeusWin32._svc_name_)
+        if self.clip2zeus is not None:
+            self.clip2zeus.quit()
+
+        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
+        self.isAlive = False
+
+if __name__ == '__main__':
+    HandleCommandLine(Clip2ZeusService)
+

clip2zeus/common.py

-from SimpleXMLRPCServer import SimpleXMLRPCServer
 from datetime import datetime, timedelta
 from threading import Thread, Event
-import logging
 import re
 
 try:
 import urllib2
 import xmlrpclib
 
+from clip2zeus.globals import *
+from clip2zeus.config import *
+
 __author__ = 'Josh VanderLinden'
-__version__ = '0.8e'
-
-APP_TITLE = 'Clip2Zeus'
-DELIM = ' \n\r<>"\''
-URL_RE = re.compile('((\w+)://([^/%%%s]+)(/?[^%s]*))' % (DELIM, DELIM), re.I | re.M)
-INVALID_DOMAINS = ('2ze.us', 'bit.ly', 'tinyurl.com', 'tr.im', 'is.gd')
-
-HEARTBEAT_INT = 30 # Interval to ensure we have a connection to 2ze.us (seconds)
-TIMEOUT_SEC = 10 # Number of seconds to wait on 2ze.us before giving up
-DEFAULT_PORT = 14694
-LOG_FILE = 'clip2zeus.log'
-LOG_LEVEL = logging.DEBUG
-FORMAT = '%(asctime)10s - %(levelname)s - %(name)s:%(lineno)d - %(message)s'
-
-logging.basicConfig(level=LOG_LEVEL,
-                    format='%(levelname)-8s %(asctime)s %(module)s:%(lineno)d %(message)s',
-                    datefmt='%d.%m %H:%M:%S',
-                    filename=LOG_FILE,
-                    filemode='a')
-logger = logging.getLogger('Clip2Zeus')
-
-def port_is_free(port):
-    """Ensures that a port is available for binding"""
-
-    try:
-        port = int(port)
-        logger.debug('Testing port %s' % port)
-        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        sock.bind(('localhost', port))
-    except socket.error, err:
-        logger.debug('Caught an exception: %s' % (err, ))
-        logger.debug(err[0], type(err[0]))
-        if err[0] in (48, 10048, 10061): # address already in use
-            return False
-        else:
-            raise err
-    else:
-        logger.debug('I was able to bind to port %s... unbinding.' % port)
-        sock.close()
-        return True
-
-class UnsupportedPlatformError(StandardError): pass
-
-class Server(SimpleXMLRPCServer):
-    """Wrapper to allow more graceful termination"""
-
-    def serve_forever(self):
-        self.quit = False
-        while not self.quit:
-            self.handle_request()
-
-    def kill(self):
-        self.quit = True
+__version__ = '0.9a'
 
 class Clip2ZeusApp(object):
 
-    EXPOSED = ('help', 'set_interval', 'shorten_urls', 'quit')
+    EXPOSED = ('help', 'get_interval', 'set_interval', 'shorten_urls', 'quit')
 
     def __init__(self, port=DEFAULT_PORT):
         """Creates the container for common functionality"""
 
-        logger.debug('Starting %s v%s' % (APP_TITLE, __version__))
+        logger.debug('Welcome to %s v%s' % (APP_TITLE, __version__))
 
         self.port = int(port)
 
         self.data = ''
+        self.server = None
         self._has_connection = False
         self.last_check = None
-        self.interval = 1
+        self.interval = config.get('main', 'interval', 1)
         self.threshold = timedelta(seconds=HEARTBEAT_INT)
         socket.setdefaulttimeout(TIMEOUT_SEC)
 
             sys.exit(1)
 
         try:
-            logger.debug('Starting server...')
+            logger.debug('Starting server on port %s...' % self.port)
             self.server = Server(('localhost', self.port), allow_none=True, logRequests=False)
             self.expose_api()
             self.server.serve_forever()
 
         pass
 
+    def get_interval(self, default=1):
+        """Returns the current polling interval in seconds"""
+
+        logger.debug('Interval requested by client: %s' % (self.interval,))
+        return config.getint('main', 'interval', default)
+
     def set_interval(self, interval):
         """Sets the clipboard polling frequency.
 
 
             if self.interval < 0:
                 raise ValueError
-        except (TypeError, ValueError):
+
+            config.set('main', 'interval', self.interval)
+        except (TypeError, ValueError), err:
+            logger.error('%s' % (err,))
             raise ValueError('Please specify an integer that is 0 or greater.')
 
     def monitor_clipboard(self):
         logger.info('Exiting.')
         self.thread_event.set()
         self.monitor_thread.join()
-        self.server.kill()
+
+        if self.server:
+            self.server.kill()
 
 class Clip2ZeusCtl(object):
     """

clip2zeus/config.py

+from ConfigParser import SafeConfigParser
+import logging
+import os
+import re
+
+CONFIG_DIR = os.path.join(os.path.expanduser('~'), '.clip2zeus')
+
+try:
+    os.makedirs(CONFIG_DIR)
+except:
+    pass # directory already exists
+
+CONFIG_FILE = os.path.join(CONFIG_DIR, 'clip2zeus.conf')
+
+class Clip2ZeusConfig(SafeConfigParser):
+
+    __instance = None
+
+    def get(self, section, option, default=None):
+        """Allows default values to be returned if necessary"""
+
+        if self.has_section(section) and self.has_option(section, option):
+            return SafeConfigParser.get(self, section, option)
+        else:
+            return default
+
+    def getint(self, section, option, default=''):
+        """Returns an integer"""
+
+        return int(self.get(section, option, default))
+
+    def getfloat(self, section, option, default=''):
+        """Returns a float"""
+
+        return float(self.get(section, option, default))
+
+    def getboolean(self, section, option, default=''):
+        """Returns a boolean"""
+
+        return bool(self.get(section, option, default))
+
+    def set(self, section, option, value):
+        """Sets some configuration value and persists to disk"""
+
+        if not self.has_section(section):
+            self.add_section(section)
+
+        SafeConfigParser.set(self, section, option, str(value))
+        self.write(open(CONFIG_FILE, 'w'))
+
+    @classmethod
+    def instance(cls):
+        if cls.__instance is None:
+            # make sure the file exists
+            if not os.path.exists(CONFIG_FILE):
+                open(CONFIG_FILE, 'w')
+
+            cls.__instance = Clip2ZeusConfig()
+            cls.__instance.read(open(CONFIG_FILE))
+
+        return cls.__instance
+
+config = Clip2ZeusConfig.instance()
+
+APP_TITLE = 'Clip2Zeus'
+DELIM = ' \n\r<>"\''
+URL_RE = re.compile('((\w+)://([^/%%%s]+)(/?[^%s]*))' % (DELIM, DELIM), re.I | re.M)
+INVALID_DOMAINS = ('2ze.us', 'bit.ly', 'tinyurl.com', 'tr.im', 'is.gd')
+
+# Interval to ensure we have a connection to 2ze.us (seconds)
+HEARTBEAT_INT = config.getint('main', 'heartbeat', 30)
+# Number of seconds to wait on 2ze.us before giving up
+TIMEOUT_SEC = config.getint('main', 'timeout', 10)
+DEFAULT_PORT = config.getint('main', 'port', 14694)
+
+#
+# Logging
+#
+
+LOG_FILE = os.path.join(CONFIG_DIR, 'clip2zeus.log')
+LOG_LEVEL = logging.DEBUG
+LOG_FORMAT = '%(levelname)-8s %(asctime)s %(module)s:%(lineno)d %(message)s'
+

clip2zeus/globals.py

+from SimpleXMLRPCServer import SimpleXMLRPCServer
+import logging
+import socket
+
+#
+# Utilities
+#
+
+class Server(SimpleXMLRPCServer):
+    """Wrapper to allow more graceful termination"""
+
+    def serve_forever(self):
+        self.quit = False
+        while not self.quit:
+            self.handle_request()
+
+    def kill(self):
+        self.quit = True
+
+def get_logger():
+    from clip2zeus.config import LOG_LEVEL, LOG_FORMAT, LOG_FILE
+    logging.basicConfig(level=LOG_LEVEL,
+                        format=LOG_FORMAT,
+                        datefmt='%d.%m %H:%M:%S',
+                        filename=LOG_FILE,
+                        filemode='a')
+    return logging.getLogger('Clip2Zeus')
+
+logger = get_logger()
+
+def port_is_free(port=None):
+    """Ensures that a port is available for binding"""
+
+    if port is None:
+        from clip2zeus.config import config
+        port = config.get('main', 'port')
+
+    try:
+        port = int(port)
+        logger.debug('Testing port %s' % (port,))
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.bind(('localhost', port))
+    except socket.error, err:
+        logger.debug('Caught an exception: %s' % (err,))
+        logger.debug(err[0], type(err[0]))
+        if err[0] in (48, 10048, 10061): # address already in use
+            return False
+        else:
+            raise err
+    else:
+        logger.debug('Port %s is available. Unbinding.' % (port,))
+        sock.close()
+        return True
+
+#
+# Exceptions
+#
+
+class UnsupportedPlatformError(StandardError):
+    pass
+
 import sys
 import tkMessageBox
 
-from clip2zeus_ctl import Clip2ZeusCtl
-from common import APP_TITLE, DEFAULT_PORT, logger
+from clip2zeus.clip2zeus_ctl import Clip2ZeusCtl
+from clip2zeus.config import config, APP_TITLE, DEFAULT_PORT
+from clip2zeus.globals import logger
 
 ID_MANUAL = 100
 ID_AUTO = 110
 
 class Clip2ZeusTk(Clip2ZeusCtl):
 
-    def __init__(self, ):
+    def __init__(self):
         super(Clip2ZeusTk, self).__init__()
 
         self.build_gui()
         self.btn_quit.grid(row=1, column=1, sticky='e')
         self.scl_interval.grid(row=2, column=0, sticky='w')
 
+        cur_interval = self.execute_command('get_interval')
+        logger.debug('Received "%s" as the current interval' % (cur_interval,))
+        #self.scl_interval.set(cur_interval)
+
     def notify(self, message):
         """Tells the user something"""
 
     def mode_selected(self, interval=-1):
         """Sets the polling mode--automatic vs manual"""
 
+        cur_interval = self.execute_command('get_interval')
+        logger.debug('Received "%s" as the current interval' % (cur_interval,))
+        logger.debug('Setting poll mode: %s' % (interval,))
         if int(interval) == 0:
             self.opt_value.set(ID_MANUAL)
             self.opt_manual.select()

clip2zeus/linux.py

 import gtk
 import time
 
-from common import Clip2ZeusApp
+from clip2zeus.common import Clip2ZeusApp
 
 class Clip2ZeusLinux(Clip2ZeusApp):
 

clip2zeus/main.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Monitors the system clipboard for text that contains URLs for conversion
+using 2ze.us
+"""
+
+from clip2zeus.common import *
+
+def main():
+    from optparse import OptionParser
+
+    parser = OptionParser()
+    parser.add_option('-p', '--port', dest='port', default=DEFAULT_PORT, help='The port for the daemon to listen on')
+    options, args = parser.parse_args()
+
+    params = dict(
+        port=options.port,
+    )
+
+    clip2zeus = Clip2ZeusApp.for_platform()
+    clip2zeus(**params).start()
+
+if __name__ == '__main__':
+    main()
+
 import subprocess
 import time
-from common import Clip2ZeusApp
+from clip2zeus.common import Clip2ZeusApp
 
 class Clip2ZeusOSX(Clip2ZeusApp):
 

clip2zeus/win32.py

 
 import time
 import win32clipboard as w
-from common import Clip2ZeusApp
+from clip2zeus.common import Clip2ZeusApp
 
 class Clip2ZeusWin32(Clip2ZeusApp):
 
 # Make sure we have the appropriate libraries
 if sys.platform in ('nt', 'win32'):
     import py2exe
-    excludes = (
+    excludes = [
         'email.Generator', 'email.Iterators', 'email.Utils',
+    ]
+
+    class Target(object):
+        def __init__(self, **kwargs):
+            self.__dict__.update(kwargs)
+            self.version = __version__
+            self.company_name = __author__
+            self.copyright = 'Copyright 2010 Josh VanderLinden'
+            self.name = APP_TITLE
+
+    service = Target(
+        description="Clip2Zeus URL Shortening Service",
+        modules=['clip2zeus.clip2zeus_service'],
+        cmdline_style='pywin32',
+    )
+
+    gui = Target(
+        description="Clip2Zeus GUI",
+        script='clip2zeus/gui.py',
+        dest_base='tkclip2zeus',
     )
 
     extra = dict(
-        console=['clip2zeus/clip2zeus.py'],
-        excludes=excludes,
+        console=[
+            'clip2zeus/clip2zeus_ctl.py',
+            'clip2zeus/main.py',
+        ],
+        service=[service],
+        windows=[gui],
     )
 elif sys.platform in ('darwin', ):
     import py2app
     extra = dict(
-        app=['clip2zeus/clip2zeus.py'],
+        app=['clip2zeus/main.py'],
     )
 
 setup(
     ],
     package_dir={'clip2zeus': 'clip2zeus'},
     packages=['clip2zeus'],
+    py_modules=['clip2zeus.common'],
     platforms=[
         'Windows',
         'OSX',
             bundle_files=1,
             includes = [
                 'simplejson',
-            ]
+                'win32serviceutil',
+                'win32service',
+                'win32clipboard',
+                'clip2zeus.common',
+                'Tkinter',
+            ],
+            excludes=excludes,
         ),
         'py2app': dict(
             iconfile='clip2zeus/res/clip2zeus.icns',