#!python """Create installable Windows packages using Twisted as an NT Service""" import sys import os.path import ConfigParser import re import shutil from twisted.python import usage, util from twisted.application.app import reactorTypes from twisted.persisted.sob import guessType # sort out what __file__ really is so py2exe can work if not os.path.isfile(__file__): __file__ = sys.executable default_icon = util.sibpath(__file__, "pysvc.ico") cftypes=('python', 'xml', 'source', 'pickle') class Tap2NtsvcOptions(usage.Options): optParameters = [['type', 'y', None, 'Config file type out of: %s' % ', '.join(cftypes)], ['name', 'n', None, 'Short name of the service (used with "net start")'], ['package_version', 'v', "1.0", 'Version string of your application'], ['display_name', 'd', None, 'Human-readable name of the service'], ['description', 'e', None, 'Longer description of the service'], ['reactor', 'r', 'default', "Which reactor to use out of: " + ", ".join(reactorTypes.keys())], ['includes', 'i', "", """\ Comma-separated list of modules to bundle into the application """], ['icon', 'c', default_icon, "Windows icon file to use"], ] optFlags = [['skip-py2exe', None, "Don't do py2exe build step (implies --skip-inno-script)"], ['skip-inno-script', None, "Don't do .iss script generation step (implies --skip-inno)"], ['skip-inno', None, "Don't do Inno compile step"], ] def __init__(self): usage.Options.__init__(self) self.warnings = [] def opt_skip_py2exe(self): self['skip-py2exe'] = 1 self.opt_skip_inno_script() def opt_skip_inno_script(self): self['skip-inno-script'] = 1 self['skip-inno'] = 1 def opt_type(self, cftype): if cftype not in cftypes: raise usage.UsageError("""\ Type must be one of [%s], not \"%s\"""" % (', '.join(cftypes), cftype)) self['type'] = cftype opt_y = opt_type def parseArgs(self, conffile): self['conffile'] = os.path.abspath(conffile) self['confbase'] = os.path.basename(conffile) try: guess = guessType(conffile) except KeyError: guess = None self['type'] = (self['type'] or guess or 'pickle') def getSynopsis(self): return "Usage: %s [options] " % __file__ def postOptions(self): if not self['name']: self['name'] = os.path.splitext(self['confbase'])[0] if not isPythonName(self['name']): raise usage.UsageError("""\ \"%s\" was used for the name, but name must consist only of letters, numbers and _. (Use a different --name argument.)""" % self['name']) if not self['display_name']: self['display_name'] = "%s run by Twisted" % self['name'] if not self['includes']: self.warnings.append("""\ --includes was not given. Most applications require at least one included \ module!""") def isPythonName(st): m = re.match('[A-Za-z_][A-Za-z_0-9]*', st) if m: return m.end() == len(st) else: return 0 def ini2dict(configname, section): cp = ConfigParser.ConfigParser() cp.read(configname) dct = {} for name in cp.options(section): dct[name] = cp.get(section, name) return dct def genFile(filename, template, options): try: outfile = file(filename, "w") except EnvironmentError: sys.exit("%s\n** Could not create file %s" % (options.getSynopsis(), filename)) outfile.write(template % options) outfile.close() def run(argv = sys.argv): try: o = Tap2NtsvcOptions() o.parseOptions(argv[1:]) except usage.UsageError, ue: sys.exit("%s\n** %s" % (o, ue)) for w in o.warnings: print "--- WWW\nWarning: %s\n--- WWW" % w svc_appended = '%ssvc' % o['name'] o['script'] = '%s.py' % svc_appended o['commandline'] = ' '.join(argv) o['dirname'] = svc_appended o['options-repr'] = repr(o) try: os.mkdir(svc_appended) print "Created directory %s" % o['dirname'] except EnvironmentError, e: if e.strerror == 'File exists': pass else: sys.exit("\ Could not create directory %s because: %s" % (o['dirname'], e.strerr)) os.chdir(o['dirname']) # generate the output files generated = {o['script'] : servicectl_template, 'setup.py' : setup_template, 'setup.cfg' : cfg_template, 'README.txt' : readme_template, 'do_inno_script.py' : do_inno_script_template, 'do_inno.py' : do_inno_template, 'Makefile' : makefile_template, } for k in generated: genFile(k, generated[k], o) try: shutil.copy2(o['conffile'], '.') except EnvironmentError, e: if e.strerror == 'File exists': pass else: sys.exit("\ Could not copy file %s because: %s" % (o['conffile'], e.strerror)) # invoke the packaging tools if not o['skip-py2exe']: sys.path.insert(0, util.sibpath(o['conffile'], '')) sys.path.insert(0, os.getcwd()) import setup setup.run('setup.py -q py2exe'.split()) if not o['skip-inno-script']: execfile("do_inno_script.py") if not o['skip-inno']: execfile("do_inno.py") final = os.path.abspath("%s\\%s-setup-%s.exe" % (svc_appended, o['name'], o['package_version'])) print "Output written to %s" % final sys.stderr.write("%s: %d warnings.\n" % (os.path.basename(argv[0]), len(o.warnings))) setup_template = '''\ ## This file was generated by tap2ntsvc, with the command line: ## %(commandline)s import sys from distutils.core import setup import py2exe scriptfile = "%(script)s" configfile = "%(confbase)s" def run(argv = sys.argv): setup_args = {"scripts": [scriptfile], "data_files": [("", [configfile]), ], } orig_argv = sys.argv sys.argv = argv setup(**setup_args) sys.argv = orig_argv if __name__ == "__main__": run() ''' cfg_template = '''\ ## This file was generated by tap2ntsvc, with the command line: ## %(commandline)s [py2exe] service=%(name)s_ServiceControl ## prune docstrings (py2exe ignores them) optimize=2 excludes=perfmon # version_companyname = # version_fileversion = # version_legalcopyright = # version_legaltrademarks = version_productversion = %(package_version)s icon = %(icon)s version_filedescription = %(description)s version_productname = %(display_name)s includes = %(includes)s ''' servicectl_template = '''\ ## This file was generated by tap2ntsvc, with the command line: ## %(commandline)s import sys import os.path import re import win32serviceutil, win32service basecf = "%(confbase)s" cftype = "%(type)s" svcname = "%(name)s" display = "%(display_name)s" reactortype = "%(reactor)s" class %(name)s_ServiceControl(win32serviceutil.ServiceFramework): _svc_name_ = svcname _svc_display_name_ = display def SvcDoRun(self): from twisted.application import app app.installReactor(reactortype) from twisted.application import service from twisted.python import util, log # look for a readable config file for cf in (util.sibpath(sys.executable, basecf), util.sibpath(__file__, basecf), basecf): try: file(cf, \'r\').close() except EnvironmentError: continue else: break logname = util.sibpath(cf, "%%s.log" %% svcname) logfile = file(logname, "a") log.startLogging(logfile) log.msg("Loading application from %%s" %% cf) %(name)s_app = service.loadApplication(cf, cftype) from twisted.internet import reactor app.startApplication(%(name)s_app, 1) reactor.run(installSignalHandlers=0) def SvcStop(self): self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) from twisted.internet import reactor reactor.callFromThread(reactor.stop) if __name__ == \'__main__\': win32serviceutil.HandleCommandLine(%(name)s_ServiceControl) ''' do_inno_script_template = r''' import inno import os.path options = %(options-repr)s filemapper = "%(name)s.fms" scr = inno.Script(**options) # write an fmlang script so future runs will operate on the (possibly # user-edited) commands list, and not a static list of files if not os.path.isfile(filemapper): scr.collect(os.path.join("dist", "%(name)ssvc")) file(filemapper, "w").write(scr.fmscript) print "Created %%s" %% filemapper else: scr.fmscript = file(filemapper).read() print "Loaded %%s" %% filemapper scr.runFileCommands() outname = "%(name)s.iss" out = file(outname, "w+") scr.writeScript(out) out.write(r"""[Run] Filename: "{app}\%(name)ssvc.exe"; Parameters: "-remove"; StatusMsg: "Installing %(name)s service" Filename: "{app}\%(name)ssvc.exe"; Parameters: "-install"; StatusMsg: "Installing %(name)s service" [UninstallRun] Filename: "{sys}\net.exe"; Parameters: "%(name)s stop" Filename: "{app}\%(name)ssvc.exe"; Parameters: "-remove" """) out.close() ''' do_inno_template = '''from inno import build; build("%(name)s.iss")\n''' readme_template = '''\ This directory contains files created by: %(commandline)s ______________ MAKING CHANGES ______________ Files in here that you are likely to modify: setup.cfg, %(name)s.fms and %(name)s.iss. -- Missing Imports -- If you get errors in the Application log that say you are missing imports, edit setup.cfg, and add the named module to the line "includes=". You can add multiple modules here, separated by commas. Then do: python setup.py py2exe; python do_inno_script.py; python do_inno.py -- Missing Data Files -- If you need to distribute data files with your application, the easiest way to add them is to edit %(name)s.fms. This file uses a *very* simple language for finding files. Supported commands are: add [] grab all filenames (not names of directories) in this dir matching glob chdir (or cd) from now on, add all entries relative to this directory diradd [] add directories matching glob (not their contents--use for empty dirs) exclude from now on, don\'t grab any files that match this glob show print the current list of dest:source mappings to stdout unexclude stop excluding this glob, if it was previously excluded "import inno.fmlang; help(inno.fmlang)" (in the Python interactive interpreter) will describe fmlang in more detail. After editing the file, you have two choices. If you GNU Make (nmake might also work): make If not, run the commands by hand: python do_inno_script.py; python do_inno.py -- Other Stuff -- You can do almost anything else you want with your distributable package by editing %(name)s.iss directly. There is a help file for Inno Setup scripts in the inno/program directory of the Innoconda distribution. After editing the file, do: python do_inno.py # (or make) ''' makefile_template = '''\ # assumes Cygwin make and environment name=%(name)s version=%(package_version)s target=$(name)-$(version)-setup.exe all: $(target) @echo "Done" $(target): $(name).iss do_inno.py python do_inno.py $(name).iss: $(name).fms do_inno_script.py setup.py setup.cfg $(name).tap python setup.py py2exe python do_inno_script.py clean: rm -rf dist build rm -f $(name).iss rm -f *.pyc rm -f $(target) '''