Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions motioneye/extra/motioneye.conf.sample
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ media_path /var/lib/motioneye
# the log level (use quiet, error, warning, info or debug)
log_level info

# whether to write logs to files in log_path (motioneye.log, motion.log)
# instead of standard error / standard output;
# the log_path setting above only has an effect when this is enabled
log_to_file false

# the IP address to listen on
# (0.0.0.0 for all interfaces, 127.0.0.1 for localhost)
listen 0.0.0.0
Expand Down
3 changes: 3 additions & 0 deletions motioneye/handlers/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ def get(self, name):

path, filename = log

if path.startswith('/') and not os.path.exists(path):
raise HTTPError(404, 'log file not found')

self.set_header('Content-Type', 'text/plain')
self.set_header('Content-Disposition', 'attachment; filename=' + filename + ';')

Expand Down
16 changes: 12 additions & 4 deletions motioneye/meyectl.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def load_settings():

config_file = None
debug = False
log_to_file = False

for i in range(1, len(sys.argv)):
arg = sys.argv[i]
Expand All @@ -111,6 +112,9 @@ def load_settings():
elif arg == '-d':
debug = True

elif arg == '-l':
log_to_file = True

conf_path_given = [False]
run_path_given = [False]
log_path_given = [False]
Expand Down Expand Up @@ -202,9 +206,14 @@ def parse_conf_line(line):
if debug:
settings.LOG_LEVEL = logging.DEBUG

if log_to_file:
settings.LOG_TO_FILE = True


def configure_logging(cmd, log_to_file=None):
if log_to_file is None:
log_to_file = settings.LOG_TO_FILE

def configure_logging(cmd, log_to_file=False):
sys.stderr.write(f'configure_logging cmd {cmd}: {log_to_file}\n')
if log_to_file or cmd != 'motioneye':
fmt = f'%(asctime)s: [{cmd}] %(levelname)8s: %(message)s'

Expand All @@ -221,7 +230,6 @@ def configure_logging(cmd, log_to_file=False):
else:
log_file = None

sys.stderr.write(f'configure logging to file: {log_file}\n')
logging.basicConfig(
filename=log_file,
level=settings.LOG_LEVEL,
Expand Down Expand Up @@ -288,7 +296,7 @@ def make_arg_parser(command=None):
)
parser.add_argument(
'-l',
help='log to file instead of standard error',
help='log to file instead of standard error, overriding the config file setting',
action='store_true',
dest='log_to_file',
)
Expand Down
14 changes: 12 additions & 2 deletions motioneye/motionctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ def find_motion():
return _motion_binary_cache


def _get_motion_log_file():
"""
Open motion's log file, or return None to let motion
inherit motionEye's stdout/stderr.
"""
if not settings.LOG_TO_FILE:
return None

return open(join(settings.LOG_PATH, 'motion.log'), 'w')


def start(deferred=False):
from motioneye import config, mjpgclient

Expand All @@ -99,7 +110,6 @@ def start(deferred=False):
logging.debug(f'starting motion executable "{binary}" version "{version}"')

motion_cfg_path = join(settings.CONF_PATH, 'motion.conf')
motion_log_path = join(settings.LOG_PATH, 'motion.log')
motion_pid_path = join(settings.RUN_PATH, 'motion.pid')

args = [binary, '-n', '-c', motion_cfg_path, '-d']
Expand All @@ -116,7 +126,7 @@ def start(deferred=False):
else: # fatal, quiet
args.append('1')

log_file = open(motion_log_path, 'w')
log_file = _get_motion_log_file()

process = Popen(
args, stdout=log_file, stderr=log_file, close_fds=True, cwd=settings.CONF_PATH
Expand Down
2 changes: 1 addition & 1 deletion motioneye/sendmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def main(parser, args):

options = parse_options(parser, args)

meyectl.configure_logging('sendmail', options.log_to_file)
meyectl.configure_logging('sendmail')

logging.debug('hello!')

Expand Down
2 changes: 1 addition & 1 deletion motioneye/sendtelegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def main(parser, args):
signal.signal(signal.SIGCHLD, signal.SIG_DFL)

options = parse_options(parser, args)
meyectl.configure_logging('telegram', options.log_to_file)
meyectl.configure_logging('telegram')
logging.debug(options)
message = 'Motion has been detected by camera "%(camera)s/%(hostname)s" at %(moment)s (%(timezone)s).'

Expand Down
7 changes: 6 additions & 1 deletion motioneye/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,12 @@ def main(parser, args, command):

options = parse_options(parser, args)

meyectl.configure_logging('motioneye', options.background or options.log_to_file)
# daemon mode redirects stdout/stderr to /dev/null,
# so logging to file is the only way to preserve any output
if options.background:
settings.LOG_TO_FILE = True

meyectl.configure_logging('motioneye')
meyectl.configure_tornado()

if command == 'start':
Expand Down
4 changes: 4 additions & 0 deletions motioneye/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
# the log level (use FATAL, ERROR, WARNING, INFO or DEBUG)
LOG_LEVEL = logging.INFO

# whether to write logs to files in log_path
# instead of standard error / standard output
LOG_TO_FILE = False

# the IP address to listen on
# (0.0.0.0 for all interfaces, 127.0.0.1 for localhost)
LISTEN = '0.0.0.0'
Expand Down
2 changes: 1 addition & 1 deletion motioneye/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def main(parser, args):

options = parse_options(parser, args)

meyectl.configure_logging('shell', options.log_to_file)
meyectl.configure_logging('shell')
meyectl.configure_tornado()

logging.debug('hello!')
Expand Down
2 changes: 1 addition & 1 deletion motioneye/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def main(parser, args):

options = parse_options(parser, args)

meyectl.configure_logging('webhook', options.log_to_file)
meyectl.configure_logging('webhook')
meyectl.configure_tornado()

logging.debug('hello!')
Expand Down
66 changes: 66 additions & 0 deletions tests/test_handlers/test_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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 <http://www.gnu.org/licenses/>.

"""Tests verifying the log handler when the requested log file is absent."""

import os
import unittest
from shutil import rmtree
from tempfile import mkdtemp
from unittest.mock import patch

from motioneye.handlers.log import LogHandler
from tests.test_handlers import HandlerTestCase


class LogHandlerTest(HandlerTestCase):
handler_cls = LogHandler

def setUp(self):
self.log_dir = mkdtemp()
super().setUp()

def tearDown(self):
super().tearDown()
rmtree(self.log_dir)

def _fetch_motion_log(self):
cookie = self.make_session_cookie('admin')
return self.fetch('/log/motion/', headers={'Cookie': cookie})

def test_missing_log_file_returns_404(self):
# with log_to_file disabled motion.log is never written (#3330);
# the handler must not blow up trying to open it
path = os.path.join(self.log_dir, 'motion.log')
with patch.dict(LogHandler.LOGS, {'motion': (path, 'motion.log')}):
response = self._fetch_motion_log()

self.assertEqual(404, response.code)

def test_existing_log_file_is_served(self):
path = os.path.join(self.log_dir, 'motion.log')
with open(path, 'w') as f:
f.write('a motion log line\n')

with patch.dict(LogHandler.LOGS, {'motion': (path, 'motion.log')}):
response = self._fetch_motion_log()

self.assertEqual(200, response.code)
self.assertEqual(b'a motion log line\n', response.body)


if __name__ == '__main__':
unittest.main()
81 changes: 81 additions & 0 deletions tests/test_meyectl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 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 <http://www.gnu.org/licenses/>.

"""Tests verifying that the log_to_file setting is read from the config file."""

import os
import sys
import unittest
from shutil import rmtree
from tempfile import mkdtemp
from unittest.mock import patch

from motioneye import meyectl, settings


class LoadSettingsLogToFileTest(unittest.TestCase):
# load_settings() assigns to these module globals directly, so patching
# cannot undo it; snapshot and restore them to keep other tests isolated
_SETTINGS = (
'CONF_PATH',
'RUN_PATH',
'LOG_PATH',
'MEDIA_PATH',
'LOG_LEVEL',
'LOG_TO_FILE',
'config_file',
)

def setUp(self):
self.conf_dir = mkdtemp()
self.saved = {name: getattr(settings, name) for name in self._SETTINGS}

def tearDown(self):
for name, value in self.saved.items():
setattr(settings, name, value)

rmtree(self.conf_dir)

def _load_settings(self, conf, argv=()):
conf_file = os.path.join(self.conf_dir, 'motioneye.conf')
with open(conf_file, 'w') as f:
f.write(conf)

with patch.object(
sys, 'argv', ['meyectl', 'startserver', '-c', conf_file, *argv]
):
meyectl.load_settings()

def test_log_to_file_disabled_by_default(self):
self._load_settings('log_level info\n')
self.assertEqual(False, settings.LOG_TO_FILE)

def test_log_to_file_false_from_config(self):
# the string is parsed as a bool, not fed to int() (#3330)
self._load_settings('log_to_file false\n')
self.assertEqual(False, settings.LOG_TO_FILE)

def test_log_to_file_true_from_config(self):
self._load_settings('log_to_file true\n')
self.assertEqual(True, settings.LOG_TO_FILE)

def test_l_argument_overrides_config(self):
self._load_settings('log_to_file false\n', argv=('-l',))
self.assertEqual(True, settings.LOG_TO_FILE)


if __name__ == '__main__':
unittest.main()
53 changes: 53 additions & 0 deletions tests/test_motionctl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 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 <http://www.gnu.org/licenses/>.

"""Tests verifying where motion's output is sent, depending on log_to_file."""

import os
import unittest
from shutil import rmtree
from tempfile import mkdtemp
from unittest.mock import patch

from motioneye import motionctl, settings


class MotionLogFileTest(unittest.TestCase):
def setUp(self):
self.log_dir = mkdtemp()

def tearDown(self):
rmtree(self.log_dir)

def test_no_log_file_when_log_to_file_disabled(self):
# None makes Popen pass motionEye's own stdout/stderr on to motion
with patch.object(settings, 'LOG_TO_FILE', False):
self.assertIsNone(motionctl._get_motion_log_file())

def test_log_file_opened_when_log_to_file_enabled(self):
with patch.object(settings, 'LOG_TO_FILE', True), patch.object(
settings, 'LOG_PATH', self.log_dir
):
log_file = motionctl._get_motion_log_file()

try:
self.assertEqual(os.path.join(self.log_dir, 'motion.log'), log_file.name)
finally:
log_file.close()


if __name__ == '__main__':
unittest.main()
Loading