From 4d2e4a92c871780ad1198e8e5c134d8fff94ddcf Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sun, 23 Aug 2015 21:20:31 +0300 Subject: [PATCH] refactored for single script (meyectl) --- extra/motioneye.conf.sample | 81 +++++ motioneye/config.py | 46 ++- motioneye/meyectl.py | 222 ++++++++++++ motioneye/mjpgclient.py | 3 + motioneye/{eventrelay.py => relayevent.py} | 46 +-- motioneye/sendmail.py | 97 +++--- motioneye/server.py | 372 ++++++++++++--------- motioneye/settings.py | 49 +-- motioneye/webhook.py | 38 +-- 9 files changed, 652 insertions(+), 302 deletions(-) create mode 100644 extra/motioneye.conf.sample create mode 100755 motioneye/meyectl.py rename motioneye/{eventrelay.py => relayevent.py} (78%) mode change 100755 => 100644 mode change 100755 => 100644 motioneye/sendmail.py mode change 100755 => 100644 motioneye/webhook.py diff --git a/extra/motioneye.conf.sample b/extra/motioneye.conf.sample new file mode 100644 index 0000000..98b3b4d --- /dev/null +++ b/extra/motioneye.conf.sample @@ -0,0 +1,81 @@ + +# static files (.css, .js etc) are served at this root url; +# change this if you run motionEye behind a reverse proxy (e.g. nginx), +# and you want static files to be served directly by it +#static_url '/static/' + +# path to the configuration directory (must be writable by motionEye) +conf_path /etc/motioneye + +# path to the directory where pid files go (must be writable by motionEye) +#run_path /var/run + +# path to the directory where log files go (must be writable by motionEye) +#log_path /var/log + +# default output path for media files (must be writable by motionEye) +#media_path /var/lib/motioneye + +# path to the motion binary to use (automatically detected by default) +#motion_binary /usr/bin/motion + +# the log level (use quiet, error, warning, info or debug) +#log_level info + +# the IP address to listen on +# (0.0.0.0 for all interfaces, 127.0.0.1 for localhost) +#listen 0.0.0.0 + +# the TCP port to listen on +#port 8765 + +# interval in seconds at which motionEye checks the SMB mounts +#mount_check_interval 300 + +# interval in seconds at which motionEye checks if motion is running +#motion_check_interval 10 + +# interval in seconds at which the janitor is called +# to remove old pictures and movies +#cleanup_interval 43200 + +# interval in seconds at which the thumbnail mechanism runs +# (set to 0 to disable) +#thumbnailer_interval 60 + +# timeout in seconds to wait for response from a remote motionEye server +#remote_request_timeout 10 + +# timeout in seconds to wait for mjpg data from the motion daemon +#mjpg_client_timeout 10 + +# timeout in seconds after which an idle mjpg client is removed +# (set to 0 to disable) +#mjpg_client_idle_timeout 10 + +# enable SMB shares (requires motionEye to run as root) +#smb_shares false + +# the directory where the SMB mount points will be created +#smb_mount_root /media + +# path to the wpa_supplicant.conf file +# (enable this to configure wifi settings from the UI) +#wpa_supplicant_conf /etc/wpa_supplicant.conf + +# path to the localtime file +# (enable this to configure the system time zone from the UI) +#local_time_file /etc/localtime + +# enables shutdown and rebooting after changing system settings +# (such as wifi settings or time zone) +#enable_reboot false + +# timeout in seconds to use when talking to the SMTP server +#smtp_timeout 60 + +# timeout in seconds to wait for zip file creation +#zip_timeout 500 + +# enable adding and removing cameras from UI +#add_remove_cameras true diff --git a/motioneye/config.py b/motioneye/config.py index 235d763..6d9c671 100644 --- a/motioneye/config.py +++ b/motioneye/config.py @@ -507,7 +507,7 @@ def add_camera(device_details): if device_details['username']: camera_config['netcam_userpass'] = device_details['username'] + ':' + device_details['password'] - camera_config['netcam_keepalive'] = device_details.get('keep_alive') + camera_config['netcam_keepalive'] = device_details.get('keep_alive', False) camera_config['netcam_tolerant_check'] = True if device_details.get('camera_index') == 'udp': @@ -609,6 +609,7 @@ def main_dict_to_ui(data): def motion_camera_ui_to_dict(ui, old_config=None): + import meyectl import smbctl old_config = dict(old_config or {}) @@ -833,17 +834,14 @@ def motion_camera_ui_to_dict(ui, old_config=None): data['@working_schedule_type'] = ui['working_schedule_type'] # event start - event_relay_path = os.path.join(settings.PROJECT_PATH, 'eventrelay.py') - event_relay_path = os.path.abspath(event_relay_path) - - on_event_start = ['%(script)s start %%t' % {'script': event_relay_path}] + on_event_start = ['%(script)s start %%t' % {'script': meyectl.find_command('relayevent')}] if ui['email_notifications_enabled']: send_mail_path = os.path.join(settings.PROJECT_PATH, 'sendmail.py') send_mail_path = os.path.abspath(send_mail_path) emails = re.sub('\\s', '', ui['email_notifications_addresses']) on_event_start.append("%(script)s '%(server)s' '%(port)s' '%(account)s' '%(password)s' '%(tls)s' '%(to)s' 'motion_start' '%%t' '%%Y-%%m-%%dT%%H:%%M:%%S' '%(timespan)s'" % { - 'script': send_mail_path, + 'script': meyectl.find_command('sendmail'), 'server': ui['email_notifications_smtp_server'], 'port': ui['email_notifications_smtp_port'], 'account': ui['email_notifications_smtp_account'], @@ -853,12 +851,10 @@ def motion_camera_ui_to_dict(ui, old_config=None): 'timespan': ui['email_notifications_picture_time_span']}) if ui['web_hook_notifications_enabled']: - web_hook_path = os.path.join(settings.PROJECT_PATH, 'webhook.py') - web_hook_path = os.path.abspath(web_hook_path) url = re.sub('\\s', '+', ui['web_hook_notifications_url']) on_event_start.append("%(script)s '%(method)s' '%(url)s'" % { - 'script': web_hook_path, + 'script': meyectl.find_command('webhook'), 'method': ui['web_hook_notifications_http_method'], 'url': url}) @@ -869,7 +865,7 @@ def motion_camera_ui_to_dict(ui, old_config=None): data['on_event_start'] = '; '.join(on_event_start) # event end - on_event_end = ['%(script)s stop %%t' % {'script': event_relay_path}] + on_event_end = ['%(script)s stop %%t' % {'script': meyectl.find_command('relayevent')}] data['on_event_end'] = '; '.join(on_event_end) @@ -1156,34 +1152,36 @@ def motion_camera_dict_to_ui(data): ui['email_notifications_picture_time_span'] = 0 command_notifications = [] for e in on_event_start: - if e.count('sendmail.py') and e.count('motion_start'): + if e.count('sendmail'): e = shlex.split(e) + if len(e) < 10: continue - + ui['email_notifications_enabled'] = True - ui['email_notifications_smtp_server'] = e[1] - ui['email_notifications_smtp_port'] = e[2] - ui['email_notifications_smtp_account'] = e[3] - ui['email_notifications_smtp_password'] = e[4] - ui['email_notifications_smtp_tls'] = e[5].lower() == 'true' - ui['email_notifications_addresses'] = e[6] + ui['email_notifications_smtp_server'] = e[-10] + ui['email_notifications_smtp_port'] = e[-9] + ui['email_notifications_smtp_account'] = e[-8] + ui['email_notifications_smtp_password'] = e[-7] + ui['email_notifications_smtp_tls'] = e[-6].lower() == 'true' + ui['email_notifications_addresses'] = e[-5] try: - ui['email_notifications_picture_time_span'] = int(e[10]) + ui['email_notifications_picture_time_span'] = int(e[-1]) except: ui['email_notifications_picture_time_span'] = 0 - elif e.count('webhook.py'): + elif e.count('webhook'): e = shlex.split(e) - if len(e) != 3: + + if len(e) < 3: continue ui['web_hook_notifications_enabled'] = True - ui['web_hook_notifications_http_method'] = e[1] - ui['web_hook_notifications_url'] = e[2] + ui['web_hook_notifications_http_method'] = e[-1] + ui['web_hook_notifications_url'] = e[-1] - elif e.count('eventrelay.py'): + elif e.count('relayevent') or e.count('eventrelay.py'): continue # ignore internal relay script else: # custom command diff --git a/motioneye/meyectl.py b/motioneye/meyectl.py new file mode 100755 index 0000000..cfa3320 --- /dev/null +++ b/motioneye/meyectl.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python + +# Copyright (c) 2013 Calin Crisan +# This file is part of motionEye. +# +# motionEye is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import logging +import os.path +import pipes +import sys + +from tornado.httpclient import AsyncHTTPClient + +# make sure motioneye is on python path +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import relayevent +import sendmail +import server +import settings +import webhook + + +_LOG_FILE = 'motioneye.log' + + +def find_command(command): + cmd = os.path.abspath(sys.argv[0]) + cmd += ' %s ' % command + cmd += ' '.join([pipes.quote(arg) for arg in sys.argv[2:]]) + + return cmd + + +def load_settings(): + # parse common comnand line argumentss + config_file = None + debug = False + + for i in xrange(1, len(sys.argv)): + arg = sys.argv[i] + next_arg = i < len(sys.argv) - 1 and sys.argv[i + 1] + if arg == '-c': + config_file = next_arg + + elif arg == '-d': + debug = True + + def parse_conf_line(line): + line = line.strip() + if not line or line.startswith('#'): + return + + parts = line.split(' ', 1) + if len(parts) != 2: + raise Exception('invalid configuration line: %s' % line) + + name, value = parts + upper_name = name.upper().replace('-', '_') + + if hasattr(settings, upper_name): + curr_value = getattr(settings, upper_name) + + if upper_name == 'LOG_LEVEL': + if value == 'quiet': + value = 100 + + else: + value = getattr(logging, value.upper(), logging.DEBUG) + + elif value.lower() == 'true': + value = True + + elif value.lower() == 'false': + value = False + + elif isinstance(curr_value, int): + value = int(value) + + elif isinstance(curr_value, float): + value = float(value) + + setattr(settings, upper_name, value) + + else: + raise Exception('unknown configuration option: %s' % name) + + if config_file: + try: + with open(config_file) as f: + for line in f: + parse_conf_line(line) + + except Exception as e: + logging.fatal('failed to read settings from "%s": %s' % (config_file, e)) + sys.exit(-1) + + else: + logging.info('no configuration file given, using built-in defaults') + + if debug: + settings.LOG_LEVEL = logging.DEBUG + + +def configure_logging(cmd, log_to_file=False): + format = '%(asctime)s: [{cmd}] %(levelname)s: %(message)s'.format(cmd=cmd) + + for h in logging.getLogger().handlers: + logging.getLogger().removeHandler(h) + + try: + if log_to_file: + log_file = os.path.join(settings.LOG_PATH, _LOG_FILE) + + else: + log_file = None + + logging.basicConfig(filename=log_file, level=settings.LOG_LEVEL, + format=format, datefmt='%Y-%m-%d %H:%M:%S') + + except Exception as e: + sys.stderr.write('failed to configure logging: %s\n' % e) + sys.exit(-1) + + logging.getLogger('tornado').setLevel(logging.WARN) + + +def configure_tornado(): + AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient', max_clients=16) + + +def make_arg_parser(command=None): + if command: + usage = description = epilog = None + + else: + usage = '%(prog)s [command] [-c CONFIG_FILE] [-d] [-v] [command options...]\n\n' + + description = 'available commands:\n' + description += ' startserver\n' + description += ' stopserver\n' + description += ' relayevent\n' + description += ' sendmail\n' + description += ' webhook\n\n' + + epilog = 'type "%(prog)s [command] -h" for help on a specific command\n\n' + + parser = argparse.ArgumentParser(prog='meyectl%s' % ((' ' + command) if command else ''), + usage=usage, description=description, epilog=epilog, + add_help=False, formatter_class=argparse.RawTextHelpFormatter) + + parser.add_argument('-c', help='use a config file instead of built-in defaults', + type=str, dest='config_file') + parser.add_argument('-d', help='enable debugging, overriding log level from config file', + action='store_true', dest='debug') + parser.add_argument('-h', help='print this help and exit', + action='help', default=argparse.SUPPRESS) + parser.add_argument('-v', help='print program version and exit', + action='version', default=argparse.SUPPRESS) + + return parser + + +def print_usage_and_exit(code): + parser = make_arg_parser() + parser.print_help(sys.stderr) + + sys.exit(code) + + +def print_version_and_exit(): + import motioneye + + sys.stderr.write('motionEye %s\n' % motioneye.VERSION) + sys.exit() + + +def main(): + for a in sys.argv: + if a == '-v': + print_version_and_exit() + + if len(sys.argv) < 2 or sys.argv[1] == '-h': + print_usage_and_exit(0) + + load_settings() + + command = sys.argv[1] + arg_parser = make_arg_parser(command) + + if command in ('startserver', 'stopserver'): + server.main(arg_parser, sys.argv[2:], command[:-6]) + + elif command == 'sendmail': + sendmail.main(arg_parser, sys.argv[2:]) + + elif command == 'relayevent': + relayevent.main(arg_parser, sys.argv[2:]) + + elif command == 'webhook': + webhook.main(arg_parser, sys.argv[2:]) + + else: + sys.stderr.write('unknown command "%s"\n\n' % command) + print_usage_and_exit(-1) + + +if __name__ == '__main__': + main() diff --git a/motioneye/mjpgclient.py b/motioneye/mjpgclient.py index 088a962..093c3cd 100644 --- a/motioneye/mjpgclient.py +++ b/motioneye/mjpgclient.py @@ -77,6 +77,9 @@ class MjpgClient(iostream.IOStream): motionctl.start(deferred=True) MjpgClient.last_erroneous_close_time = now + + # remove the cached picture + MjpgClient.last_jpgs.pop(self._camera_id, None) def _check_error(self): if self.socket is None: diff --git a/motioneye/eventrelay.py b/motioneye/relayevent.py old mode 100755 new mode 100644 similarity index 78% rename from motioneye/eventrelay.py rename to motioneye/relayevent.py index 42c02f3..2a91ba5 --- a/motioneye/eventrelay.py +++ b/motioneye/relayevent.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2013 Calin Crisan # This file is part of motionEye. @@ -28,16 +27,6 @@ sys.path.append(os.path.join(os.path.dirname(sys.argv[0]),'src')) import settings import utils -from motioneye import _configure_settings, _configure_logging - - -_configure_settings() -_configure_logging(module='eventrelay') - - -def print_usage(): - print 'Usage: eventrelay.py ' - def get_admin_credentials(): # this shortcut function is a bit faster than using the config module functions @@ -97,36 +86,31 @@ def get_admin_credentials(): return admin_username, admin_password -# def compute_signature(method, uri, body, key): -# parts = list(urlparse.urlsplit(uri)) -# query = [q for q in urlparse.parse_qsl(parts[3]) if (q[0] != 'signature')] -# query.sort(key=lambda q: q[0]) -# query = urllib.urlencode(query) -# parts[0] = parts[1] = '' -# parts[3] = query -# uri = urlparse.urlunsplit(parts) -# -# return hashlib.sha1('%s:%s:%s:%s' % (method, uri, body or '', key)).hexdigest().lower() +def parse_options(parser, args): + parser.add_argument('event', help='the name of the event to relay') + parser.add_argument('thread_id', help='the id of the thread') + return parser.parse_args(args) + -if __name__ == '__main__': - if len(sys.argv) < 3: - print_usage() - sys.exit(-1) +def main(parser, args): + import meyectl + + options = parse_options(parser, args) - event = sys.argv[1] - thread_id = sys.argv[2] + meyectl.configure_logging('relayevent') + meyectl.configure_tornado() logging.debug('hello!') - logging.debug('event = %s' % event) - logging.debug('thread_id = %s' % thread_id) + logging.debug('event = %s' % options.event) + logging.debug('thread_id = %s' % options.thread_id) admin_username, admin_password = get_admin_credentials() uri = '/_relay_event/?event=%(event)s&thread_id=%(thread_id)s&_username=%(username)s' % { 'username': admin_username, - 'thread_id': thread_id, - 'event': event} + 'thread_id': options.thread_id, + 'event': options.event} signature = utils.compute_signature('POST', uri, '', admin_password) diff --git a/motioneye/sendmail.py b/motioneye/sendmail.py old mode 100755 new mode 100644 index 4844c08..df59222 --- a/motioneye/sendmail.py +++ b/motioneye/sendmail.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2013 Calin Crisan # This file is part of motionEye. @@ -22,7 +21,6 @@ import os import re import smtplib import socket -import sys import time from email import Encoders @@ -33,12 +31,6 @@ from tornado.ioloop import IOLoop import settings -from motioneye import _configure_settings, _configure_logging, _configure_signals - -_configure_settings() -_configure_signals() -_configure_logging(module='sendmail') - import config import mediafiles import tzctl @@ -54,7 +46,7 @@ subjects = { def send_mail(server, port, account, password, tls, to, subject, message, files): - conn = smtplib.SMTP(server, port, timeout=getattr(settings, 'SMTP_TIMEOUT', 60)) + conn = smtplib.SMTP(server, port, timeout=settings.SMTP_TIMEOUT) if tls: conn.starttls() @@ -130,67 +122,62 @@ def make_message(subject, message, camera_id, moment, timespan, callback): mediafiles.list_media(camera_config, media_type='picture', callback=on_media_files) -def print_usage(): - print 'Usage: sendmail.py [timespan]' - +def parse_options(parser, args): + parser.add_argument('server', help='address of the SMTP server') + parser.add_argument('port', help='port for the SMTP connection') + parser.add_argument('account', help='SMTP account name (username)') + parser.add_argument('password', help='SMTP account password') + parser.add_argument('tls', help='"true" to use TLS') + parser.add_argument('to', help='the email recipient(s)') + parser.add_argument('msg_id', help='the identifier of the message') + parser.add_argument('camera_id', help='the id of the camera') + parser.add_argument('moment', help='the moment in ISO-8601 format') + parser.add_argument('timespan', help='picture collection time span') -if __name__ == '__main__': - if len(sys.argv) < 10: - print_usage() - sys.exit(-1) + return parser.parse_args(args) - server = sys.argv[1] - port = int(sys.argv[2]) - account = sys.argv[3] - password = sys.argv[4] - tls = sys.argv[5].lower() == 'true' - to = sys.argv[6] - msg_id = sys.argv[7] - camera_id = sys.argv[8] - moment = sys.argv[9] - try: - timespan = int(sys.argv[10]) - except: - timespan = 0 +def main(parser, args): + import meyectl + + options = parse_options(parser, args) + + meyectl.configure_logging('sendmail') + meyectl.configure_tornado() logging.debug('hello!') - message = messages.get(msg_id) - subject = subjects.get(msg_id) - if not message or not subject: - logging.error('unknown message id') - sys.exit(-1) + options.port = int(options.port) + options.tls = options.tls.lower() == 'true' + options.timespan = int(options.timespan) + message = messages.get(options.msg_id) + subject = subjects.get(options.msg_id) + options.moment = datetime.datetime.strptime(options.moment, '%Y-%m-%dT%H:%M:%S') - moment = datetime.datetime.strptime(moment, '%Y-%m-%dT%H:%M:%S') - - logging.debug('server = %s' % server) - logging.debug('port = %s' % port) - logging.debug('account = %s' % account) + logging.debug('server = %s' % options.server) + logging.debug('port = %s' % options.port) + logging.debug('account = %s' % options.account) logging.debug('password = ******') - logging.debug('server = %s' % server) - logging.debug('tls = %s' % tls) - logging.debug('to = %s' % to) - logging.debug('msg_id = %s' % msg_id) - logging.debug('camera_id = %s' % camera_id) - logging.debug('moment = %s' % moment.strftime('%Y-%m-%d %H:%M:%S')) + logging.debug('server = %s' % options.server) + logging.debug('tls = %s' % str(options.tls).lower()) + logging.debug('to = %s' % options.to) + logging.debug('msg_id = %s' % options.msg_id) + logging.debug('camera_id = %s' % options.camera_id) + logging.debug('moment = %s' % options.moment.strftime('%Y-%m-%d %H:%M:%S')) logging.debug('smtp timeout = %d' % settings.SMTP_TIMEOUT) - logging.debug('timespan = %d' % timespan) + logging.debug('timespan = %d' % options.timespan) - if not to: - logging.info('no email address specified') - sys.exit(0) - - to = [t.strip() for t in re.split('[,;| ]', to)] + to = [t.strip() for t in re.split('[,;| ]', options.to)] to = [t for t in to if t] io_loop = IOLoop.instance() def on_message(subject, message, files): try: - send_mail(server, port, account, password, tls, to, subject, message, files) + send_mail(options.server, options.port, options.account, options.password, + options.tls, options.to, subject, message, files) logging.info('email sent') - + except Exception as e: logging.error('failed to send mail: %s' % e, exc_info=True) @@ -199,9 +186,9 @@ if __name__ == '__main__': def ioloop_timeout(): io_loop.stop() - make_message(subject, message, camera_id, moment, timespan, on_message) + make_message(subject, message, options.camera_id, options.moment, options.timespan, on_message) io_loop.add_timeout(datetime.timedelta(seconds=settings.SMTP_TIMEOUT), ioloop_timeout) io_loop.start() - logging.debug('bye!') \ No newline at end of file + logging.debug('bye!') diff --git a/motioneye/server.py b/motioneye/server.py index 76b385b..3913873 100644 --- a/motioneye/server.py +++ b/motioneye/server.py @@ -15,86 +15,178 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import argparse +import atexit import datetime +import logging import multiprocessing -import os.path +import os import signal -import sys +import sys +import time -from tornado.httpclient import AsyncHTTPClient from tornado.web import Application import handlers -import logging import settings import template -def load_settings(): - # TODO use optparse -# length = len(sys.argv) - 1 -# for i in xrange(length): -# arg = sys.argv[i + 1] -# -# if not arg.startswith('--'): -# continue -# -# next_arg = None -# if i < length - 1: -# next_arg = sys.argv[i + 2] -# -# name = arg[2:].upper().replace('-', '_') -# -# if hasattr(settings, name): -# curr_value = getattr(settings, name) -# -# if next_arg.lower() == 'debug': -# next_arg = logging.DEBUG -# -# elif next_arg.lower() == 'info': -# next_arg = logging.INFO -# -# elif next_arg.lower() == 'warn': -# next_arg = logging.WARN -# -# elif next_arg.lower() == 'error': -# next_arg = logging.ERROR -# -# elif next_arg.lower() == 'fatal': -# next_arg = logging.FATAL -# -# elif next_arg.lower() == 'true': -# next_arg = True -# -# elif next_arg.lower() == 'false': -# next_arg = False -# -# elif isinstance(curr_value, int): -# next_arg = int(next_arg) -# -# elif isinstance(curr_value, float): -# next_arg = float(next_arg) -# -# setattr(settings, name, next_arg) -# -# else: -# return arg[2:] +_PID_FILE = 'motioneye.pid' - if not os.path.exists(settings.CONF_PATH): - logging.fatal('config directory "%s" does not exist' % settings.CONF_PATH) - sys.exit(-1) + +class Daemon(object): + def __init__(self, pid_file, run_callback=None): + self.pid_file = pid_file + self.run_callback = run_callback + + def daemonize(self): + # first fork + try: + if os.fork() > 0: # parent + sys.exit(0) + + except OSError, e: + sys.stderr.write('fork() failed: %s\n' % e.strerror) + sys.exit(-1) + + # separate from parent + os.chdir('/') + os.setsid() + os.umask(0) + + # second fork + try: + if os.fork() > 0: # parent + sys.exit(0) + + except OSError, e: + sys.stderr.write('fork() failed: %s\n' % e.strerror) + sys.exit(-1) + + # redirect standard file descriptors + sys.stdout.flush() + sys.stderr.flush() + si = file('/dev/null', 'r') + so = file('/dev/null', 'a+') + se = file('/dev/null', 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + # pid file + atexit.register(self.del_pid) + with open(self.pid_file, 'w') as f: + f.write('%s\n' % os.getpid()) + + def del_pid(self): + try: + os.remove(self.pid_file) + + except: + pass - if not os.path.exists(settings.RUN_PATH): - logging.fatal('pid directory "%s" does not exist' % settings.RUN_PATH) - sys.exit(-1) + def running(self): + try: + with open(self.pid_file) as f: + pid = int(f.read().strip()) - if not os.path.exists(settings.LOG_PATH): - logging.fatal('log directory "%s" does not exist' % settings.LOG_PATH) - sys.exit(-1) + except: + return None - if not os.path.exists(settings.MEDIA_PATH): - logging.fatal('media directory "%s" does not exist' % settings.MEDIA_PATH) - sys.exit(-1) + try: + os.kill(pid, 0) + return pid + + except: + return None + + def start(self): + if self.running(): + sys.stderr.write('server is already running\n') + sys.exit(-1) + + self.daemonize() + sys.stdout.write('server started\n') + self.run_callback() + + def stop(self): + pid = self.running() + if not pid: + sys.stderr.write('server is not running\n') + sys.exit(-1) + + try: + os.kill(pid, signal.SIGTERM) + + except Exception as e: + sys.stderr.write('failed to terminate server: %s\n' % e) + + for i in xrange(50): # @UnusedVariable + try: + os.kill(pid, 0) + time.sleep(0.1) + + except OSError as e: + if str(e).count('No such process'): + self.del_pid() + sys.stdout.write('server stopped\n') + break + + else: + sys.stderr.write('failed to terminate server: %s\n' % e) + sys.exit(-11) + + else: + sys.stderr.write('server failed to stop, killing it\n') + try: + os.kill(pid, signal.SIGKILL) + + except: + pass + + +def _log_request(handler): + if handler.get_status() < 400: + log_method = logging.debug + + elif handler.get_status() < 500: + log_method = logging.warning + + else: + log_method = logging.error + + request_time = 1000.0 * handler.request.request_time() + log_method("%d %s %.2fms", handler.get_status(), + handler._request_summary(), request_time) + +application = Application( + [ + (r'^/$', handlers.MainHandler), + (r'^/config/main/(?Pset|get)/?$', handlers.ConfigHandler), + (r'^/config/(?P\d+)/(?Pget|set|rem|set_preview)/?$', handlers.ConfigHandler), + (r'^/config/(?Padd|list|backup|restore)/?$', handlers.ConfigHandler), + (r'^/picture/(?P\d+)/(?Pcurrent|list|frame)/?$', handlers.PictureHandler), + (r'^/picture/(?P\d+)/(?Pdownload|preview|delete)/(?P.+?)/?$', handlers.PictureHandler), + (r'^/picture/(?P\d+)/(?Pzipped|timelapse|delete_all)/(?P.+?)/?$', handlers.PictureHandler), + (r'^/movie/(?P\d+)/(?Plist)/?$', handlers.MovieHandler), + (r'^/movie/(?P\d+)/(?Pdownload|preview|delete)/(?P.+?)/?$', handlers.MovieHandler), + (r'^/movie/(?P\d+)/(?Pdelete_all)/(?P.+?)/?$', handlers.MovieHandler), + (r'^/_relay_event/?$', handlers.RelayEventHandler), + (r'^/log/(?P\w+)/?$', handlers.LogHandler), + (r'^/update/?$', handlers.UpdateHandler), + (r'^/power/(?Pshutdown|reboot)/?$', handlers.PowerHandler), + (r'^/version/?$', handlers.VersionHandler), + (r'^/login/?$', handlers.LoginHandler), + (r'^.*$', handlers.NotFoundHandler), + ], + debug=False, + log_function=_log_request, + static_path=settings.STATIC_PATH, + static_url_prefix=settings.STATIC_URL +) + +template.add_context('STATIC_URL', settings.STATIC_URL) def configure_signals(): @@ -116,24 +208,23 @@ def configure_signals(): signal.signal(signal.SIGCHLD, child_handler) -def configure_logging(module=None): - if module: - format = '%(asctime)s: [{module}] %(levelname)s: %(message)s'.format(module=module) - - else: - format = '%(asctime)s: %(levelname)s: %(message)s' - - logging.basicConfig(filename=None, level=settings.LOG_LEVEL, - format=format, datefmt='%Y-%m-%d %H:%M:%S') - - logging.getLogger('tornado').setLevel(logging.WARN) - +def test_requirements(): + if not os.access(settings.CONF_PATH, os.W_OK): + logging.fatal('config directory "%s" does not exist or is not writable' % settings.CONF_PATH) + sys.exit(-1) + + if not os.access(settings.RUN_PATH, os.W_OK): + logging.fatal('pid directory "%s" does not exist or is not writable' % settings.RUN_PATH) + sys.exit(-1) -def configure_tornado(): - AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient', max_clients=16) + if not os.access(settings.LOG_PATH, os.W_OK): + logging.fatal('log directory "%s" does not exist or is not writable' % settings.LOG_PATH) + sys.exit(-1) + if not os.access(settings.MEDIA_PATH, os.W_OK): + logging.fatal('media directory "%s" does not exist or is not writable' % settings.MEDIA_PATH) + sys.exit(-1) -def test_requirements(): if os.geteuid() != 0: if settings.SMB_SHARES: print('SMB_SHARES require root privileges') @@ -244,12 +335,40 @@ def start_thumbnailer(): logging.info('thumbnailer started') -def run_server(): +def parse_options(parser, args): + parser.add_argument('-b', help='start the server in background (daemonize)', + action='store_true', dest='background', default=False) + + return parser.parse_args(args) + + +def run(): import cleanup import motionctl + import motioneye + import smbctl import thumbnailer import tornado.ioloop - import smbctl + + configure_signals() + test_requirements() + + logging.info('hello! this is motionEye server %s' % motioneye.VERSION) + + if settings.SMB_SHARES: + + stop, start = smbctl.update_mounts() # @UnusedVariable + if start: + start_motion() + + else: + start_motion() + + start_cleanup() + start_wsswitch() + + if settings.THUMBNAILER_INTERVAL: + start_thumbnailer() application.listen(settings.PORT, settings.LISTEN) logging.info('server started') @@ -274,78 +393,27 @@ def run_server(): smbctl.umount_all() logging.info('SMB shares unmounted') - -def log_request(handler): - if handler.get_status() < 400: - log_method = logging.debug - - elif handler.get_status() < 500: - log_method = logging.warning - - else: - log_method = logging.error - - request_time = 1000.0 * handler.request.request_time() - log_method("%d %s %.2fms", handler.get_status(), - handler._request_summary(), request_time) - - -application = Application( - [ - (r'^/$', handlers.MainHandler), - (r'^/config/main/(?Pset|get)/?$', handlers.ConfigHandler), - (r'^/config/(?P\d+)/(?Pget|set|rem|set_preview)/?$', handlers.ConfigHandler), - (r'^/config/(?Padd|list|backup|restore)/?$', handlers.ConfigHandler), - (r'^/picture/(?P\d+)/(?Pcurrent|list|frame)/?$', handlers.PictureHandler), - (r'^/picture/(?P\d+)/(?Pdownload|preview|delete)/(?P.+?)/?$', handlers.PictureHandler), - (r'^/picture/(?P\d+)/(?Pzipped|timelapse|delete_all)/(?P.+?)/?$', handlers.PictureHandler), - (r'^/movie/(?P\d+)/(?Plist)/?$', handlers.MovieHandler), - (r'^/movie/(?P\d+)/(?Pdownload|preview|delete)/(?P.+?)/?$', handlers.MovieHandler), - (r'^/movie/(?P\d+)/(?Pdelete_all)/(?P.+?)/?$', handlers.MovieHandler), - (r'^/_relay_event/?$', handlers.RelayEventHandler), - (r'^/log/(?P\w+)/?$', handlers.LogHandler), - (r'^/update/?$', handlers.UpdateHandler), - (r'^/power/(?Pshutdown|reboot)/?$', handlers.PowerHandler), - (r'^/version/?$', handlers.VersionHandler), - (r'^/login/?$', handlers.LoginHandler), - (r'^.*$', handlers.NotFoundHandler), - ], - debug=False, - log_function=log_request, - static_path=settings.STATIC_PATH, - static_url_prefix=settings.STATIC_URL -) - -template.add_context('STATIC_URL', settings.STATIC_URL) + logging.info('bye!') -def main(): - import motioneye +def main(parser, args, command): + import meyectl - load_settings() - configure_signals() - configure_logging() - test_requirements() - configure_tornado() - - logging.info('hello! this is motionEye %s' % motioneye.VERSION) + options = parse_options(parser, args) - if settings.SMB_SHARES: - import smbctl - - stop, start = smbctl.update_mounts() # @UnusedVariable - if start: - start_motion() - - else: - start_motion() - - start_cleanup() - start_wsswitch() - - if settings.THUMBNAILER_INTERVAL: - start_thumbnailer() - - run_server() + meyectl.configure_logging('motioneye', options.background) + meyectl.configure_tornado() + + if command == 'start': + if options.background: + daemon = Daemon( + pid_file=os.path.join(settings.RUN_PATH, _PID_FILE), + run_callback=run) + daemon.start() + + else: + run() - logging.info('bye!') + elif command == 'stop': + daemon = Daemon(pid_file=os.path.join(settings.RUN_PATH, _PID_FILE)) + daemon.stop() diff --git a/motioneye/settings.py b/motioneye/settings.py index 9e7f68b..1a5fa83 100644 --- a/motioneye/settings.py +++ b/motioneye/settings.py @@ -14,13 +14,15 @@ TEMPLATE_PATH = os.path.join(PROJECT_PATH, 'templates') # the static files directory STATIC_PATH = os.path.join(PROJECT_PATH, 'static') -# static files (.css, .js etc) are served at this root url +# static files (.css, .js etc) are served at this root url; +# change this if you run motionEye behind a reverse proxy (e.g. nginx), +# and you want static files to be served directly by it STATIC_URL = '/static/' -# path to the config directory; must be writable +# path to the configuration directory (must be writable by motionEye) CONF_PATH = [sys.prefix, ''][sys.prefix == '/usr'] + '/etc/motioneye' -# pid files go here +# path to the directory where pid files go (must be writable by motionEye) for d in ['/run', '/var/run', '/tmp', '/var/tmp']: if os.path.exists(d): RUN_PATH = d @@ -29,7 +31,7 @@ for d in ['/run', '/var/run', '/tmp', '/var/tmp']: else: RUN_PATH = PROJECT_PATH -# log files go here +# path to the directory where log files go (must be writable by motionEye) for d in ['/log', '/var/log', '/tmp', '/var/tmp']: if os.path.exists(d): LOG_PATH = d @@ -38,16 +40,17 @@ for d in ['/log', '/var/log', '/tmp', '/var/tmp']: else: LOG_PATH = RUN_PATH -# default output path for media files +# default output path for media files (must be writable by motionEye) MEDIA_PATH = RUN_PATH -# path to motion binary (automatically detected if not set) +# path to the motion binary to use (automatically detected by default) MOTION_BINARY = None -# the log level +# the log level (use FATAL, ERROR, WARNING, INFO or DEBUG) LOG_LEVEL = logging.INFO -# IP addresses to listen on +# the IP address to listen on +# (0.0.0.0 for all interfaces, 127.0.0.1 for localhost) LISTEN = '0.0.0.0' # the TCP port to listen on @@ -59,40 +62,46 @@ MOUNT_CHECK_INTERVAL = 300 # interval in seconds at which motionEye checks if motion is running MOTION_CHECK_INTERVAL = 10 -# interval in seconds at which the janitor is called to remove old pictures and movies +# interval in seconds at which the janitor is called +# to remove old pictures and movies CLEANUP_INTERVAL = 43200 -# interval in seconds at which the thumbnail mechanism runs (set to 0 to disable) +# interval in seconds at which the thumbnail mechanism runs +# (set to 0 to disable) THUMBNAILER_INTERVAL = 60 -# timeout in seconds when waiting for response from a remote motionEye server +# timeout in seconds to wait for response from a remote motionEye server REMOTE_REQUEST_TIMEOUT = 10 -# timeout in seconds when waiting for mjpg data from the motion daemon +# timeout in seconds to wait for mjpg data from the motion daemon MJPG_CLIENT_TIMEOUT = 10 -# timeout in seconds after which an idle mjpg client is removed (set to 0 to disable) +# timeout in seconds after which an idle mjpg client is removed +# (set to 0 to disable) MJPG_CLIENT_IDLE_TIMEOUT = 10 -# enable SMB shares (requires root) +# enable SMB shares (requires motionEye to run as root) SMB_SHARES = False -# the directory where the SMB mounts will be created +# the directory where the SMB mount points will be created SMB_MOUNT_ROOT = '/media' -# path to a wpa_supplicant.conf file if wifi settings UI is desired +# path to the wpa_supplicant.conf file +# (enable this to configure wifi settings from the UI) WPA_SUPPLICANT_CONF = None -# path to a localtime file if time zone settings UI is desired +# path to the localtime file +# (enable this to configure the system time zone from the UI) LOCAL_TIME_FILE = None -# enables shutdown and rebooting after changing system settings (such as wifi settings or system updates) +# enables shutdown and rebooting after changing system settings +# (such as wifi settings or time zone) ENABLE_REBOOT = False -# the timeout in seconds to use when talking to a SMTP server +# timeout in seconds to use when talking to the SMTP server SMTP_TIMEOUT = 60 -# the time to wait for zip file creation +# timeout in seconds to wait for zip file creation ZIP_TIMEOUT = 500 # enable adding and removing cameras from UI diff --git a/motioneye/webhook.py b/motioneye/webhook.py old mode 100755 new mode 100644 index 016a0db..2767c6c --- a/motioneye/webhook.py +++ b/motioneye/webhook.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # Copyright (c) 2013 Calin Crisan # This file is part of motionEye. @@ -17,45 +16,44 @@ # along with this program. If not, see . import logging -import sys import urllib2 import urlparse import settings -from motioneye import _configure_settings, _configure_logging +def parse_options(parser, args): + parser.add_argument('method', help='the HTTP method to use') + parser.add_argument('url', help='the URL for the request') -_configure_settings() -_configure_logging() + return parser.parse_args(args) -def print_usage(): - print 'Usage: webhook.py ' - - -if __name__ == '__main__': - if len(sys.argv) < 3: - print_usage() - sys.exit(-1) +def main(parser, args): + import meyectl - method = sys.argv[1] - url = sys.argv[2] + options = parse_options(parser, args) + + meyectl.configure_logging('webhook') + meyectl.configure_tornado() - logging.debug('method = %s' % method) - logging.debug('url = %s' % url) + logging.debug('hello!') + logging.debug('method = %s' % options.method) + logging.debug('url = %s' % options.url) - if method == 'POST': - parts = urlparse.urlparse(url) + if options.method == 'POST': + parts = urlparse.urlparse(options.url) data = parts.query else: data = None - request = urllib2.Request(url, data) + request = urllib2.Request(options.url, data) try: urllib2.urlopen(request, timeout=settings.REMOTE_REQUEST_TIMEOUT) logging.debug('webhook successfully called') except Exception as e: logging.error('failed to call webhook: %s' % e) + + logging.debug('bye!') -- 2.39.5