-
Notifications
You must be signed in to change notification settings - Fork 2
/
coffeecup.py
217 lines (166 loc) · 7.18 KB
/
coffeecup.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import logging, os, os.path
import subprocess, shlex, threading, time, signal
from os.path import dirname, basename, join
from subprocess import CalledProcessError
log = logging.getLogger(__name__)
DEFAULT_COFFEE_CMD = '/usr/local/bin/coffee'
class CoffeeScriptMiddleware(object):
""" CoffeScript WSGI Middleware
"""
coffee_cmd = None
static_dir = ''
enabled = True
watch = True
find_on_startup=False
app = None
config = None
watching = None
def __init__(self, app, config=None, watch=True, find_on_startup=False, static_dir=None, coffee_cmd=None):
self.app = app
self.config = config or {}
self.coffee_cmd = coffee_cmd or DEFAULT_COFFEE_CMD
self.static_dir = static_dir or config.get('pylons.paths', {}).get('static_files', os.getcwd())
self.watching = set()
self.enabled = True
self.watch = watch
self.find_on_startup = find_on_startup
if self.watch and self.find_on_startup:
self.findScripts()
if self.watch:
self.startWatching()
def __call__(self, environ, start_response):
reqpath = environ.get('PATH_INFO', '')
newpath = None
if reqpath.endswith('.js'):
realpath, newpath, coffeepath = self.handleJavaScript(reqpath)
elif reqpath.endswith('.coffee'):
realpath, newpath, coffeepath = self.handleCoffeeScript(reqpath)
if newpath:
environ['coffeecup.original_path'] = reqpath
environ['coffeecup.real_path'] = realpath
environ['coffeecup.source_path'] = coffeepath
environ['coffeecup.new_path'] = environ['PATH_INFO'] = newpath
return self.app(environ, start_response)
def handleJavaScript(self, reqpath):
realpath = self.toStaticPath(reqpath)
coffeepath = join(dirname(realpath), realpath[:-3] + '.coffee')
if not os.path.exists(realpath) and os.path.exists(coffeepath):
log.debug('Compiling Coffee file for JS request: %s', reqpath)
try:
self.compileScript(coffeepath)
self.watching.add(coffeepath)
except CalledProcessError as ex:
log.error(
'\n\t'.join([
'Error compiling CoffeeScript:',
'reqpath: %s',
'realpath: %s',
'newpath: %s',
'command: %s',
'retcode: %s',
'\t%s',
]),
reqpath, realpath, newpath,
ex.cmd, ex.returncode, (ex.output or '').replace('\n', '\n\t\t') )
return realpath, reqpath, coffeepath
def handleCoffeeScript(self, reqpath):
realpath = self.toStaticPath(reqpath)
newpath = join( dirname(reqpath), reqpath[:-7] + '.js')
log.debug('Coffee request: %s --> %s', reqpath, newpath)
try:
self.compileScript(realpath)
self.watching.add(realpath)
except CalledProcessError as ex:
log.error(
'\n\t'.join([
'Error compiling CoffeeScript:',
'reqpath: %s',
'realpath: %s',
'newpath: %s',
'command: %s',
'retcode: %s',
'\t%s',
]),
reqpath, realpath, newpath,
ex.cmd, ex.returncode, (ex.output or '').replace('\n', '\n\t\t') )
return realpath, newpath, realpath
def compileScript(self, coffeefile):
# Let the 404 drop through if the .coffee file doesn't exist
if not os.path.exists(coffeefile):
return
jsfile = join(dirname(coffeefile), coffeefile[:-7] + '.js')
if os.path.exists(jsfile):
log.debug('File extant: %s', jsfile)
else:
log.debug('Compiling %s --> %s', coffeefile, jsfile)
cmd = "%s -c '%s'" % (self.coffee_cmd, coffeefile)
subprocess.check_call(shlex.split(cmd), stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
def toStaticPath(self, virtpath):
if virtpath.startswith('/'):
virtpath = virtpath[1:]
return os.path.abspath( join(self.static_dir, virtpath) )
def findScripts(self, root=None, ignore=('.svn', '.git',)):
root = root or self.static_dir
for path, dirs, files in os.walk(root, followlinks=True):
self.watching.update( join(path, f) for f in files if f.endswith('.coffee') )
for d in ignore:
if d in dirs: dirs.remove(d)
log.debug('Found %s scripts to watch: %s', len(self.watching), ', '.join(self.watching))
def startWatching(self):
self.watcher = CoffeeWatcher(self)
# self.watcher.setDaemon(True)
self.watcher.start()
class CoffeeWatcher(threading.Thread):
""" Background Thread to run coffee -w. """
update_interval = 10.0
parent = None
watching = None
running = False
current = None
process = None
def __init__(self, parent, update_interval=10, liveness_interval=0.05):
super(CoffeeWatcher, self).__init__(name='coffee-watcher')
self.parent = parent
self.coffee_cmd = parent.coffee_cmd
self.watching = parent.watching
self.current = set()
self.update_interval = update_interval
self.liveness_interval = liveness_interval
def run(self):
self.running = True
try:
modulus = int(self.update_interval / self.liveness_interval)
i = 100*modulus / 80
while self.running and self.parent:
if i % modulus == 0:
self.verifyFiles()
if self.watching != self.current:
self.restartWatcher()
time.sleep(self.liveness_interval)
i += 1
self.running = False
except:
log.exception('Error in CoffeeWatcher!')
self.running = False
def verifyFiles(self):
for f in list(self.watching):
if not os.path.exists(f):
log.debug('File Removed: No longer watching %s', f)
self.watching.remove(f)
def restartWatcher(self):
self.current = self.watching.copy()
cmd = '%s -w -c %s' % (self.coffee_cmd, ' '.join("'%s'" % f for f in self.current ))
if self.process:
log.info('Restarting Watcher: watched set changed! %s', cmd)
self.process.send_signal(signal.SIGINT)
else:
log.info('Starting Watcher: %s', cmd)
self.process = subprocess.Popen(shlex.split(cmd))
def stop(self):
if self.process:
self.process.terminate()
log.debug('Watcher shutting down')
self.process = None
self.running = False
def __del__(self):
self.stop()