hgbook

view en/examples/run-example @ 79:53427f786a0f

Make run-example time out if shell seems to get stuck.
author Bryan O'Sullivan <bos@serpentine.com>
date Mon Sep 04 14:31:17 2006 -0700 (2006-09-04)
parents a893de25bc24
children b476081a9c04
line source
1 #!/usr/bin/env python
2 #
3 # This program takes something that resembles a shell script and runs
4 # it, spitting input (commands from the script) and output into text
5 # files, for use in examples.
7 import cStringIO
8 import errno
9 import getopt
10 import os
11 import pty
12 import re
13 import select
14 import shutil
15 import signal
16 import stat
17 import sys
18 import tempfile
19 import time
21 tex_subs = {
22 '\\': '\\textbackslash{}',
23 '{': '\\{',
24 '}': '\\}',
25 }
27 def gensubs(s):
28 start = 0
29 for i, c in enumerate(s):
30 sub = tex_subs.get(c)
31 if sub:
32 yield s[start:i]
33 start = i + 1
34 yield sub
35 yield s[start:]
37 def tex_escape(s):
38 return ''.join(gensubs(s))
40 class example:
41 shell = '/usr/bin/env bash'
42 prompt = '__run_example_prompt__ '
43 pi_re = re.compile(r'#\$\s*(name):\s*(.*)$')
45 timeout = 5
47 def __init__(self, name, verbose):
48 self.name = name
49 self.verbose = verbose
50 self.poll = select.poll()
52 def parse(self):
53 '''yield each hunk of input from the file.'''
54 fp = open(self.name)
55 cfp = cStringIO.StringIO()
56 for line in fp:
57 cfp.write(line)
58 if not line.rstrip().endswith('\\'):
59 yield cfp.getvalue()
60 cfp.seek(0)
61 cfp.truncate()
63 def status(self, s):
64 sys.stdout.write(s)
65 if not s.endswith('\n'):
66 sys.stdout.flush()
68 def send(self, s):
69 if self.verbose:
70 print >> sys.stderr, '>', self.debugrepr(s)
71 while s:
72 count = os.write(self.cfd, s)
73 s = s[count:]
75 def debugrepr(self, s):
76 rs = repr(s)
77 limit = 60
78 if len(rs) > limit:
79 return ('%s%s ... [%d bytes]' % (rs[:limit], rs[0], len(s)))
80 else:
81 return rs
83 timeout = 5
85 def read(self):
86 events = self.poll.poll(self.timeout * 1000)
87 if not events:
88 print >> sys.stderr, '[timed out after %d seconds]' % self.timeout
89 os.kill(self.pid, signal.SIGHUP)
90 return ''
91 return os.read(self.cfd, 1024)
93 def receive(self):
94 out = cStringIO.StringIO()
95 while True:
96 try:
97 if self.verbose:
98 sys.stderr.write('< ')
99 s = self.read()
100 except OSError, err:
101 if err.errno == errno.EIO:
102 return ''
103 raise
104 if self.verbose:
105 print >> sys.stderr, self.debugrepr(s)
106 out.write(s)
107 s = out.getvalue()
108 if s.endswith(self.prompt):
109 return s.replace('\r\n', '\n')[:-len(self.prompt)]
111 def sendreceive(self, s):
112 self.send(s)
113 r = self.receive()
114 if r.startswith(s):
115 r = r[len(s):]
116 return r
118 def run(self):
119 ofp = None
120 basename = os.path.basename(self.name)
121 self.status('running %s ' % basename)
122 tmpdir = tempfile.mkdtemp(prefix=basename)
124 rcfile = os.path.join(tmpdir, '.hgrc')
125 rcfp = open(rcfile, 'w')
126 print >> rcfp, '[ui]'
127 print >> rcfp, "username = Bryan O'Sullivan <bos@serpentine.com>"
129 rcfile = os.path.join(tmpdir, '.bashrc')
130 rcfp = open(rcfile, 'w')
131 print >> rcfp, 'PS1="%s"' % self.prompt
132 print >> rcfp, 'unset HISTFILE'
133 print >> rcfp, 'export EXAMPLE_DIR="%s"' % os.getcwd()
134 print >> rcfp, 'export LANG=C'
135 print >> rcfp, 'export LC_ALL=C'
136 print >> rcfp, 'export TZ=GMT'
137 print >> rcfp, 'export HGRC="%s/.hgrc"' % tmpdir
138 print >> rcfp, 'export HGRCPATH=$HGRC'
139 print >> rcfp, 'cd %s' % tmpdir
140 rcfp.close()
141 sys.stdout.flush()
142 sys.stderr.flush()
143 self.pid, self.cfd = pty.fork()
144 if self.pid == 0:
145 cmdline = ['/usr/bin/env', 'bash', '--noediting', '--noprofile',
146 '--norc']
147 try:
148 os.execv(cmdline[0], cmdline)
149 except OSError, err:
150 print >> sys.stderr, '%s: %s' % (cmdline[0], err.strerror)
151 sys.stderr.flush()
152 os._exit(0)
153 self.poll.register(self.cfd, select.POLLIN | select.POLLERR |
154 select.POLLHUP)
155 try:
156 try:
157 # eat first prompt string from shell
158 self.read()
159 # setup env and prompt
160 self.sendreceive('source %s\n' % rcfile)
161 for hunk in self.parse():
162 # is this line a processing instruction?
163 m = self.pi_re.match(hunk)
164 if m:
165 pi, rest = m.groups()
166 if pi == 'name':
167 self.status('.')
168 out = rest
169 assert os.sep not in out
170 if out:
171 ofp = open('%s.%s.out' % (self.name, out), 'w')
172 else:
173 ofp = None
174 elif hunk.strip():
175 # it's something we should execute
176 output = self.sendreceive(hunk)
177 if not ofp:
178 continue
179 # first, print the command we ran
180 if not hunk.startswith('#'):
181 nl = hunk.endswith('\n')
182 hunk = ('$ \\textbf{%s}' %
183 tex_escape(hunk.rstrip('\n')))
184 if nl: hunk += '\n'
185 ofp.write(hunk)
186 # then its output
187 ofp.write(tex_escape(output))
188 self.status('\n')
189 open(self.name + '.run', 'w')
190 except:
191 print >> sys.stderr, '(killed)'
192 os.kill(self.pid, signal.SIGKILL)
193 pid, rc = os.wait()
194 raise
195 else:
196 try:
197 output = self.sendreceive('exit\n')
198 if ofp:
199 ofp.write(output)
200 os.close(self.cfd)
201 except IOError:
202 pass
203 os.kill(self.pid, signal.SIGTERM)
204 pid, rc = os.wait()
205 if rc:
206 if os.WIFEXITED(rc):
207 print >> sys.stderr, '(exit %s)' % os.WEXITSTATUS(rc)
208 elif os.WIFSIGNALED(rc):
209 print >> sys.stderr, '(signal %s)' % os.WTERMSIG(rc)
210 return rc
211 finally:
212 shutil.rmtree(tmpdir)
214 def main(path='.'):
215 opts, args = getopt.getopt(sys.argv[1:], 'v', ['verbose'])
216 verbose = False
217 for o, a in opts:
218 if o in ('-v', '--verbose'):
219 verbose = True
220 errs = 0
221 if args:
222 for a in args:
223 try:
224 st = os.lstat(a)
225 except OSError, err:
226 print >> sys.stderr, '%s: %s' % (a, err.strerror)
227 errs += 1
228 continue
229 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
230 if example(a, verbose).run():
231 errs += 1
232 else:
233 print >> sys.stderr, '%s: not a file, or not executable' % a
234 errs += 1
235 return errs
236 for name in os.listdir(path):
237 if name == 'run-example' or name.startswith('.'): continue
238 if name.endswith('.out') or name.endswith('~'): continue
239 if name.endswith('.run'): continue
240 pathname = os.path.join(path, name)
241 st = os.lstat(pathname)
242 if stat.S_ISREG(st.st_mode) and st.st_mode & 0111:
243 if example(pathname, verbose).run():
244 errs += 1
245 print >> open(os.path.join(path, '.run'), 'w'), time.asctime()
246 return errs
248 if __name__ == '__main__':
249 sys.exit(main())