#!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] <filename>" % __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 [<glob>]
grab all filenames (not names of directories) in this dir matching glob
chdir (or cd) <dir>
from now on, add all entries relative to this directory
diradd [<glob>]
add directories matching glob (not their contents--use for empty dirs)
exclude <glob>
from now on, don\'t grab any files that match this glob
show
print the current list of dest:source mappings to stdout
unexclude <glob>
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)
'''
syntax highlighted by Code2HTML, v. 0.9.1