root/trunk/justrecord.py

Revision 1152, 10.6 kB (checked in by alban, 1 year ago)

Add configuration file support via --config option. Fixes #1

  • Property svn:executable set to
Line 
1 #!/usr/bin/env python
2
3 #
4 # justrecord.py
5 #
6 # GPL - Alban Peignier - 2007
7
8 import alsaaudio
9 import sys
10 import signal, os
11 import time
12 import wave
13 import optparse
14 from optparse import OptionParser
15 import pwd, grp
16 import syslog
17 import subprocess
18 import ConfigParser
19
20 class Recorder:
21
22   def __init__(self, config):
23     self.channels = config.int('channels')
24     self.rate = config.int('rate')
25     self.basename = config['basename']
26     self.time_limit = config.int('time_limit')
27     self.basedir = config['basedir']
28     self.after_script = config['after_script']
29
30     self.output_file_index = 0
31     self.output_stopped_until = None
32
33   def init_alsa(self):
34     self.inp = alsaaudio.PCM(alsaaudio.PCM_CAPTURE,alsaaudio.PCM_NONBLOCK)
35
36     self.inp.setchannels(self.channels)
37     self.inp.setrate(self.rate)
38     self.inp.setformat(alsaaudio.PCM_FORMAT_S16_LE)
39
40     self.inp.setperiodsize(160)
41
42   def init_signals(self):
43     def handler_stop(signum, frame):
44       log('signal : exit')
45       self.recording = False
46
47     # Set the signal handler and a 5-second alarm
48     signal.signal(signal.SIGTERM, handler_stop)
49     signal.signal(signal.SIGINT, handler_stop)
50
51     def handler_nextfile(signum, frame):
52       log('signal : new file')
53       self.open_next_file()
54
55     signal.signal(signal.SIGUSR1, handler_nextfile)
56
57   def close_current_file(self):
58     if self.output_file is not None:
59       self.output_file.close()
60       if self.after_script is not None:
61         log("execute %s %s" % (self.after_script, self.output_file_name))
62         try:
63           subprocess.Popen([self.after_script, self.output_file_name])
64         except OSError, e:
65           log("error: after script failed: %d (%s)" % (e.errno, e.strerror))
66
67     self.output_file = None
68     self.output_file_name = None
69
70   def open_next_file(self):
71     if self.output_stopped_until is not None:
72       if self.output_stopped_until > time.time():
73         # silently return
74         return
75       else:
76         # end of this alarm
77         self.output_stopped_until = None
78
79     if self.output_file is not None:
80       self.close_current_file()
81
82     output_file_name = os.path.join(self.basedir , self.next_file_name())
83
84     try:
85       self.output_file = wave.open(output_file_name, 'wb')
86       log('new file : %s' % output_file_name)
87
88       self.output_file.setnchannels(self.channels)
89       self.output_file.setframerate(self.rate)
90       self.output_file.setsampwidth(2)
91
92       self.output_file_index += 1
93       self.output_file_name = output_file_name
94     except IOError, e:
95       log("error: can't open new file: %s - %d (%s) - retry in 60s" % (output_file_name, e.errno, e.strerror))
96       self.output_stopped_until = time.time() + 60
97
98   def next_file_name(self):
99     info = {
100       'time': time.strftime('%Y-%m-%d-%H-%M-%S'),
101       'index' : self.output_file_index,
102       'base' : self.basename
103     }
104     return '%(base)s-%(index)03d-%(time)s.wav' % info
105
106   def run(self):
107     log("start (output: %s, time: %d, channels: %d, rate: %d)" % (self.basename, self.time_limit, self.channels, self.rate ))
108
109     self.init_alsa()
110     self.init_signals()
111
112     position_limit = self.time_limit * self.rate
113     self.output_file = None
114     self.recording = True
115
116     while self.recording:
117       l,data = self.inp.read()
118
119       if l:
120         if self.output_file is None:
121           self.open_next_file()
122
123         # if output_file is still None, we're in IO error
124         if self.output_file is not None:
125           self.output_file.writeframesraw(data)
126
127           output_position = self.output_file.tell()
128           if output_position > position_limit:
129             self.open_next_file()
130
131       time.sleep(.001)
132
133     self.close_current_file()
134     log('exit')
135
136 class Log:
137   """file like for writes with auto flush after each write
138   to ensure that everything is logged, even during an
139   unexpected exit."""
140   def __init__(self, f):
141     self.f = f
142   def write(self, s):
143     self.f.write(s)
144     self.f.flush()
145
146 class Daemon:
147
148   def __init__(self, config, main):
149     self.uid = config['uid']
150     self.gid = config['gid']
151     self.pidfile = config['pidfile']
152     self.basedir = config['basedir']
153     self.logfile = config['logfile']
154     self.main = main
155
156     if isinstance(self.uid, basestring):
157       if self.uid.isdigit():
158         self.uid = int(self.uid)
159       else:
160                                 self.uid = pwd.getpwnam(self.uid).pw_uid
161
162     if isinstance(self.gid, basestring):
163       if self.gid.isdigit():
164         self.gid = int(self.gid)
165       else:
166         self.gid = grp.getgrnam(self.gid).gr_gid
167
168     if self.logfile is None:
169       self.logfile = "/dev/null"
170
171   def start(self):
172     log('start daemon')
173     # do the UNIX double-fork magic, see Stevens' "Advanced
174     # Programming in the UNIX Environment" for details (ISBN 0201563177)
175     try:
176       pid = os.fork()
177       if pid > 0:
178           # exit first parent
179           sys.exit(0)
180     except OSError, e:
181       log("fork failed: %d (%s)" % (e.errno, e.strerror))
182       sys.exit(1)
183
184     # decouple from parent environment
185     os.chdir("/")   #don't prevent unmounting....
186     os.setsid()
187     os.umask(0)
188
189     # do second fork
190     try:
191         pid = os.fork()
192         if pid > 0:
193             # exit from second parent
194             if self.pidfile is not None:
195               open(self.pidfile,'w').write("%d\n"%pid)
196             sys.exit(0)
197     except OSError, e:
198         log("fork 2 failed: %d (%s)" % (e.errno, e.strerror))
199         sys.exit(1)
200
201     # start the daemon main loop
202     os.chdir(self.basedir)
203
204     #redirect input to /dev/null
205     sys.stdin = open("/dev/null", 'r')
206     #redirect outputs to a logfile
207     sys.stdout = sys.stderr = Log(open(self.logfile, 'a+'))
208     #ensure the that the daemon runs a normal user
209     os.setegid(self.gid)
210     os.seteuid(self.uid)
211
212     #start the user program here:
213     self.main()
214
215 class Logger:
216
217   def __init__(self, console, syslogEnabled, logfile):
218     self.console = console
219     self.syslog = syslogEnabled
220     self.logfile = None
221
222     if self.syslog:
223       syslog.openlog("justrecord", syslog.LOG_PID, syslog.LOG_USER)
224     if logfile is not None:
225       self.logfile = open(logfile, 'a+')
226
227   def log(self, message):
228     timed_message = "%s %s" % (time.strftime('%b %d %T'), message)
229
230     if self.logfile is not None:
231       self.logfile.write("%s\n" % timed_message)
232     if self.logfile is None or self.console:
233       print timed_message
234     if self.syslog:
235       syslog.syslog(message)
236
237   def close(self):
238     if self.logfile is not None:
239       self.logfile.close()
240     if self.syslog:
241       syslog.closelog()
242
243 class Config:
244
245   def __init__(self, default_options):
246     self.options = {}
247     self.update(default_options)
248
249   def update(self, options):
250     if isinstance(options, optparse.Values):
251       for entry in options.__dict__.iteritems():
252         if entry[1] is not None:
253           self.options[entry[0]] = entry[1]
254     else:
255       self.options.update(options)
256
257     return self
258
259   def load_ini(self, inifile):
260     config_parser = ConfigParser.ConfigParser()
261     if config_parser.read([inifile]) is []:
262       return False
263
264     ini_options = {}
265     for config_section in config_parser.sections():
266       for config_entry in config_parser.items(config_section):
267         ini_options[config_entry[0]] = config_entry[1]
268     self.update(ini_options)
269     return True
270
271   def __getitem__(self, key):
272     return self.options[key]
273
274   def string(self, key):
275     return self.options[key]
276
277   def int(self, key):
278     return int(self.options[key])
279
280   def bool(self, key):
281     value = self.options[key]
282     if isinstance(value, basestring):
283       return value.lower() in [ 'true', 'on', '1' ]
284     return value is True
285
286 if __name__ == "__main__":
287
288   config = Config({
289     'basename': 'record',
290     'channels': 2,
291     'after_script': None,
292     'rate': 44100,
293     'time_limit': 30 * 60,
294     'basedir': os.getcwd(),
295     'daemon': False,
296     'console': False,
297     'syslog': False,
298     'uid': os.getuid(),
299     'gid': os.getgid(),
300     'pidfile': None,
301     'logfile': None
302   })
303
304   parser = OptionParser()
305   parser.add_option("-o", "--output", dest="basename",
306                     help="The output basename. The default is 'record'.")
307   parser.add_option("-c", "--config", dest="config", default=None,
308                     help="The configuration file.")
309   parser.add_option("-n", "--channels", dest="channels", type="int",
310                     help="The number of channels. The default is %d channels." % config['channels'])
311   parser.add_option("-r", "--rate", dest="rate",
312                     choices=["48000", "44100", "22000"],
313                     help="The sampling rate in Hertz. The default rate is %d Hertz" % config['rate'])
314   parser.add_option("-t", "--time", dest="time_limit", type="int",
315                     help="The time length of recording file in seconds. The default time length is 1800 seconds (30 minutes).")
316   parser.add_option("-b", "--base-dir", dest="basedir",
317                     help="The directory where the recording files are created. The default directory is the current one.")
318   parser.add_option("-d", "--daemon", action="store_true", dest="daemon",
319                     help="Starts as daemon. Disabled by default")
320   parser.add_option("-C", "--console", action="store_true", dest="console",
321                     help="Logs messages to the console even if a logfile is specified.")
322   parser.add_option("-s", "--syslog", action="store_true", dest="syslog",
323                     help="Logs messages to syslog.")
324   parser.add_option("-u", "--uid", dest="uid",
325                     help="Change to this username/uid before starting the recorder in daemon mode. The default is the current user.")
326   parser.add_option("-g", "--gid", dest="gid",
327                     help="Change to this group/gid before starting the recorder in daemon mode. The default is the current group.")
328   parser.add_option("-p", "--pidfile", dest="pidfile",
329                     help="The file used to store the pid in daemon mode.")
330   parser.add_option("-l", "--logfile", dest="logfile",
331                     help="The file used to log messages.")
332   parser.add_option("-A", "--after-script", dest="after_script",
333                     help="Specifies a script to be executed each time a recording file is finished.")
334   (commandline_options, args) = parser.parse_args()
335
336   if commandline_options.config is not None:
337     if not config.load_ini(commandline_options.config):
338       log("error: can't read config file: %s" % commandline_options.config)
339       sys.exit(1)
340
341   config.update(commandline_options)
342
343   logger = Logger(config.bool('console'), config.bool('syslog'), config['logfile'])
344   recorder = Recorder(config)
345
346   def log(message):
347     logger.log(message)
348
349   log("config: %s" % config.options)
350
351   def main():
352     recorder.run()
353     logger.close()
354
355   daemon = config.bool('daemon')
356   if daemon:
357     Daemon(config, main).start()
358   else:
359     main()
Note: See TracBrowser for help on using the browser.