forked from platypush/platypush
Large refactor + stability fixes for the external process control logic.
This commit is contained in:
parent
46245e851f
commit
f51beb271e
13 changed files with 273 additions and 60 deletions
|
@ -1,10 +1,5 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from platypush.app import main
|
from platypush.runner import main
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main(*sys.argv[1:])
|
main(*sys.argv[1:])
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
||||||
|
|
4
platypush/app/__init__.py
Normal file
4
platypush/app/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from ._app import Application, main
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["Application", "main"]
|
7
platypush/app/__main__.py
Normal file
7
platypush/app/__main__.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ._app import main
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main(*sys.argv[1:]))
|
|
@ -1,24 +1,26 @@
|
||||||
|
from contextlib import contextmanager
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Optional, Sequence
|
from typing import Optional, Sequence
|
||||||
|
|
||||||
from .bus import Bus
|
from platypush.bus import Bus
|
||||||
from .bus.redis import RedisBus
|
from platypush.bus.redis import RedisBus
|
||||||
from .cli import parse_cmdline
|
from platypush.cli import parse_cmdline
|
||||||
from .commands import CommandStream
|
from platypush.commands import CommandStream
|
||||||
from .config import Config
|
from platypush.config import Config
|
||||||
from .context import register_backends, register_plugins
|
from platypush.context import register_backends, register_plugins
|
||||||
from .cron.scheduler import CronScheduler
|
from platypush.cron.scheduler import CronScheduler
|
||||||
from .entities import init_entities_engine, EntitiesEngine
|
from platypush.entities import init_entities_engine, EntitiesEngine
|
||||||
from .event.processor import EventProcessor
|
from platypush.event.processor import EventProcessor
|
||||||
from .logger import Logger
|
from platypush.logger import Logger
|
||||||
from .message.event import Event
|
from platypush.message.event import Event
|
||||||
from .message.event.application import ApplicationStartedEvent
|
from platypush.message.event.application import ApplicationStartedEvent
|
||||||
from .message.request import Request
|
from platypush.message.request import Request
|
||||||
from .message.response import Response
|
from platypush.message.response import Response
|
||||||
from .utils import get_enabled_plugins, get_redis_conf
|
from platypush.utils import get_enabled_plugins, get_redis_conf
|
||||||
|
|
||||||
log = logging.getLogger('platypush')
|
log = logging.getLogger('platypush')
|
||||||
|
|
||||||
|
@ -87,10 +89,6 @@ class Application:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.pidfile = pidfile
|
self.pidfile = pidfile
|
||||||
if pidfile:
|
|
||||||
with open(pidfile, 'w') as f:
|
|
||||||
f.write(str(os.getpid()))
|
|
||||||
|
|
||||||
self.bus: Optional[Bus] = None
|
self.bus: Optional[Bus] = None
|
||||||
self.redis_queue = redis_queue or RedisBus.DEFAULT_REDIS_QUEUE
|
self.redis_queue = redis_queue or RedisBus.DEFAULT_REDIS_QUEUE
|
||||||
self.config_file = config_file
|
self.config_file = config_file
|
||||||
|
@ -98,7 +96,9 @@ class Application:
|
||||||
self._logsdir = (
|
self._logsdir = (
|
||||||
os.path.abspath(os.path.expanduser(logsdir)) if logsdir else None
|
os.path.abspath(os.path.expanduser(logsdir)) if logsdir else None
|
||||||
)
|
)
|
||||||
|
|
||||||
Config.init(self.config_file)
|
Config.init(self.config_file)
|
||||||
|
Config.set('ctrl_sock', ctrl_sock)
|
||||||
|
|
||||||
if workdir:
|
if workdir:
|
||||||
Config.set('workdir', os.path.abspath(os.path.expanduser(workdir)))
|
Config.set('workdir', os.path.abspath(os.path.expanduser(workdir)))
|
||||||
|
@ -244,36 +244,66 @@ class Application:
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stops the backends and the bus."""
|
"""Stops the backends and the bus."""
|
||||||
from .plugins import RunnablePlugin
|
from platypush.plugins import RunnablePlugin
|
||||||
|
|
||||||
log.info('Stopping the application')
|
log.info('Stopping the application')
|
||||||
|
backends = (self.backends or {}).copy().values()
|
||||||
|
runnable_plugins = [
|
||||||
|
plugin
|
||||||
|
for plugin in get_enabled_plugins().values()
|
||||||
|
if isinstance(plugin, RunnablePlugin)
|
||||||
|
]
|
||||||
|
|
||||||
if self.backends:
|
for backend in backends:
|
||||||
for backend in self.backends.values():
|
|
||||||
backend.stop()
|
backend.stop()
|
||||||
|
|
||||||
for plugin in get_enabled_plugins().values():
|
for plugin in runnable_plugins:
|
||||||
if isinstance(plugin, RunnablePlugin):
|
|
||||||
plugin.stop()
|
plugin.stop()
|
||||||
|
|
||||||
|
for backend in backends:
|
||||||
|
backend.wait_stop()
|
||||||
|
|
||||||
|
for plugin in runnable_plugins:
|
||||||
|
plugin.wait_stop()
|
||||||
|
|
||||||
|
if self.entities_engine:
|
||||||
|
self.entities_engine.stop()
|
||||||
|
self.entities_engine.wait_stop()
|
||||||
|
self.entities_engine = None
|
||||||
|
|
||||||
|
if self.cron_scheduler:
|
||||||
|
self.cron_scheduler.stop()
|
||||||
|
self.cron_scheduler.wait_stop()
|
||||||
|
self.cron_scheduler = None
|
||||||
|
|
||||||
if self.bus:
|
if self.bus:
|
||||||
self.bus.stop()
|
self.bus.stop()
|
||||||
self.bus = None
|
self.bus = None
|
||||||
|
|
||||||
if self.cron_scheduler:
|
|
||||||
self.cron_scheduler.stop()
|
|
||||||
self.cron_scheduler = None
|
|
||||||
|
|
||||||
if self.entities_engine:
|
|
||||||
self.entities_engine.stop()
|
|
||||||
self.entities_engine = None
|
|
||||||
|
|
||||||
if self.start_redis:
|
if self.start_redis:
|
||||||
self._stop_redis()
|
self._stop_redis()
|
||||||
|
|
||||||
def run(self):
|
log.info('Exiting application')
|
||||||
"""Start the daemon."""
|
|
||||||
from . import __version__
|
@contextmanager
|
||||||
|
def _open_pidfile(self):
|
||||||
|
if self.pidfile:
|
||||||
|
try:
|
||||||
|
with open(self.pidfile, 'w') as f:
|
||||||
|
f.write(str(os.getpid()))
|
||||||
|
except OSError as e:
|
||||||
|
log.warning('Failed to create PID file %s: %s', self.pidfile, e)
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
if self.pidfile:
|
||||||
|
try:
|
||||||
|
os.remove(self.pidfile)
|
||||||
|
except OSError as e:
|
||||||
|
log.warning('Failed to remove PID file %s: %s', self.pidfile, e)
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
from platypush import __version__
|
||||||
|
|
||||||
if not self.no_capture_stdout:
|
if not self.no_capture_stdout:
|
||||||
sys.stdout = Logger(log.info)
|
sys.stdout = Logger(log.info)
|
||||||
|
@ -312,9 +342,17 @@ class Application:
|
||||||
self.bus.poll()
|
self.bus.poll()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
log.info('SIGINT received, terminating application')
|
log.info('SIGINT received, terminating application')
|
||||||
|
# Ignore other SIGINT signals
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
finally:
|
finally:
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run the application."""
|
||||||
|
|
||||||
|
with self._open_pidfile():
|
||||||
|
self._run()
|
||||||
|
|
||||||
|
|
||||||
def main(*args: str):
|
def main(*args: str):
|
||||||
"""
|
"""
|
||||||
|
@ -327,12 +365,7 @@ def main(*args: str):
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
log.info('Application stopped')
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(main(*sys.argv[1:]))
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
|
@ -31,7 +31,7 @@ class CommandStream(ControllableProcess):
|
||||||
"""Close the client socket after this amount of seconds."""
|
"""Close the client socket after this amount of seconds."""
|
||||||
|
|
||||||
def __init__(self, path: Optional[str] = None):
|
def __init__(self, path: Optional[str] = None):
|
||||||
super().__init__(name='platypush-cmd-stream')
|
super().__init__(name='platypush:cmd:stream')
|
||||||
self.path = os.path.abspath(os.path.expanduser(path or self._default_sock_path))
|
self.path = os.path.abspath(os.path.expanduser(path or self._default_sock_path))
|
||||||
self._sock: Optional[socket.socket] = None
|
self._sock: Optional[socket.socket] = None
|
||||||
self._cmd_queue: Queue["Command"] = Queue()
|
self._cmd_queue: Queue["Command"] = Queue()
|
||||||
|
@ -64,6 +64,7 @@ class CommandStream(ControllableProcess):
|
||||||
sock.bind(self.path)
|
sock.bind(self.path)
|
||||||
os.chmod(self.path, 0o600)
|
os.chmod(self.path, 0o600)
|
||||||
sock.listen(1)
|
sock.listen(1)
|
||||||
|
self.start()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, *_, **__):
|
def __exit__(self, *_, **__):
|
||||||
|
|
|
@ -3,6 +3,5 @@ manifest:
|
||||||
install:
|
install:
|
||||||
pip:
|
pip:
|
||||||
- py-cpuinfo
|
- py-cpuinfo
|
||||||
- psutil
|
|
||||||
package: platypush.plugins.system
|
package: platypush.plugins.system
|
||||||
type: plugin
|
type: plugin
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from logging import getLogger
|
import logging
|
||||||
from multiprocessing import Event, Process, RLock
|
from multiprocessing import Event, Process, RLock
|
||||||
from os import getpid
|
from os import getpid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
@ -17,8 +17,9 @@ class ControllableProcess(Process, ABC):
|
||||||
def __init__(self, *args, timeout: Optional[float] = None, **kwargs):
|
def __init__(self, *args, timeout: Optional[float] = None, **kwargs):
|
||||||
kwargs['name'] = kwargs.get('name', self.__class__.__name__)
|
kwargs['name'] = kwargs.get('name', self.__class__.__name__)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.logger = getLogger(self.name)
|
self.logger = logging.getLogger(self.name)
|
||||||
self._should_stop = Event()
|
self._should_stop = Event()
|
||||||
self._stop_lock = RLock()
|
self._stop_lock = RLock()
|
||||||
self._should_restart = False
|
self._should_restart = False
|
||||||
|
@ -60,15 +61,13 @@ class ControllableProcess(Process, ABC):
|
||||||
|
|
||||||
:param timeout: The maximum time to wait for the process to stop.
|
:param timeout: The maximum time to wait for the process to stop.
|
||||||
"""
|
"""
|
||||||
timeout = timeout if timeout is not None else self.timeout
|
timeout = timeout if timeout is not None else self._kill_timeout
|
||||||
with self._stop_lock:
|
with self._stop_lock:
|
||||||
self._should_stop.set()
|
self._should_stop.set()
|
||||||
self.on_stop()
|
self.on_stop()
|
||||||
|
|
||||||
if self.pid == getpid():
|
|
||||||
return # Prevent termination deadlock
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if self.pid != getpid():
|
||||||
self.wait_stop(timeout=timeout)
|
self.wait_stop(timeout=timeout)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
|
@ -76,6 +75,7 @@ class ControllableProcess(Process, ABC):
|
||||||
self.terminate()
|
self.terminate()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if self.pid != getpid():
|
||||||
self.wait_stop(timeout=self._kill_timeout)
|
self.wait_stop(timeout=self._kill_timeout)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
|
|
17
platypush/runner/__init__.py
Normal file
17
platypush/runner/__init__.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from ._runner import ApplicationRunner
|
||||||
|
|
||||||
|
|
||||||
|
def main(*args: str):
|
||||||
|
"""
|
||||||
|
Main application entry point.
|
||||||
|
|
||||||
|
This is usually the entry point that you want to use to start your
|
||||||
|
application, rather than :meth:`platypush.app.main`, as this entry point
|
||||||
|
wraps the main application in a controllable process.
|
||||||
|
"""
|
||||||
|
|
||||||
|
app_runner = ApplicationRunner()
|
||||||
|
app_runner.run(*args)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["ApplicationRunner", "main"]
|
5
platypush/runner/__main__.py
Normal file
5
platypush/runner/__main__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import main
|
||||||
|
|
||||||
|
main(sys.argv[1:])
|
59
platypush/runner/_app.py
Normal file
59
platypush/runner/_app.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from platypush.process import ControllableProcess
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationProcess(ControllableProcess):
|
||||||
|
"""
|
||||||
|
Controllable process wrapper interface for the main application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args: str, pidfile: str, **kwargs):
|
||||||
|
super().__init__(name='platypush', **kwargs)
|
||||||
|
|
||||||
|
self.logger = logging.getLogger('platypush')
|
||||||
|
self.args = args
|
||||||
|
self.pidfile = pidfile
|
||||||
|
|
||||||
|
def __enter__(self) -> "ApplicationProcess":
|
||||||
|
self.start()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *_, **__):
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
@override
|
||||||
|
def main(self):
|
||||||
|
self.logger.info('Starting application...')
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
['python', '-m', 'platypush.app', *self.args],
|
||||||
|
stdin=sys.stdin,
|
||||||
|
stdout=sys.stdout,
|
||||||
|
stderr=sys.stderr,
|
||||||
|
) as app:
|
||||||
|
try:
|
||||||
|
app.wait()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@override
|
||||||
|
def on_stop(self):
|
||||||
|
try:
|
||||||
|
with open(self.pidfile, 'r') as f:
|
||||||
|
pid = int(f.read().strip())
|
||||||
|
except (OSError, ValueError):
|
||||||
|
pid = None
|
||||||
|
|
||||||
|
if not pid:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.kill(pid, signal.SIGINT)
|
||||||
|
except OSError:
|
||||||
|
pass
|
91
platypush/runner/_runner.py
Normal file
91
platypush/runner/_runner.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from threading import Thread
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from platypush.cli import parse_cmdline
|
||||||
|
from platypush.commands import CommandStream
|
||||||
|
|
||||||
|
from ._app import ApplicationProcess
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationRunner:
|
||||||
|
"""
|
||||||
|
Runner for the main application.
|
||||||
|
|
||||||
|
It wraps the main application and provides an interface to control it
|
||||||
|
externally.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_default_timeout = 0.5
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
|
||||||
|
self.logger = logging.getLogger('platypush:runner')
|
||||||
|
self._proc: Optional[ApplicationProcess] = None
|
||||||
|
|
||||||
|
def _listen(self, stream: CommandStream):
|
||||||
|
"""
|
||||||
|
Listens for external application commands and executes them.
|
||||||
|
"""
|
||||||
|
while self.is_running:
|
||||||
|
cmd = stream.read(timeout=self._default_timeout)
|
||||||
|
if not cmd:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.logger.info(cmd)
|
||||||
|
Thread(target=cmd, args=(self,)).start()
|
||||||
|
|
||||||
|
def _print_version(self):
|
||||||
|
from platypush import __version__
|
||||||
|
|
||||||
|
print(__version__)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def _run(self, *args: str) -> None:
|
||||||
|
parsed_args = parse_cmdline(args)
|
||||||
|
|
||||||
|
if parsed_args.version:
|
||||||
|
self._print_version()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
with (
|
||||||
|
CommandStream(parsed_args.ctrl_sock) as stream,
|
||||||
|
ApplicationProcess(
|
||||||
|
*args, pidfile=parsed_args.pidfile, timeout=self._default_timeout
|
||||||
|
) as self._proc,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
self._listen(stream)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.should_restart:
|
||||||
|
self.logger.info('Restarting application...')
|
||||||
|
continue
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
def run(self, *args: str) -> None:
|
||||||
|
try:
|
||||||
|
self._run(*args)
|
||||||
|
finally:
|
||||||
|
self._proc = None
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self._proc is not None:
|
||||||
|
self._proc.stop()
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
if self._proc is not None:
|
||||||
|
self._proc.mark_for_restart()
|
||||||
|
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
return bool(self._proc and self._proc.is_alive())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_restart(self) -> bool:
|
||||||
|
return self._proc.should_restart if self._proc is not None else False
|
|
@ -11,6 +11,7 @@ frozendict
|
||||||
marshmallow
|
marshmallow
|
||||||
marshmallow_dataclass
|
marshmallow_dataclass
|
||||||
paho-mqtt
|
paho-mqtt
|
||||||
|
psutil
|
||||||
python-dateutil
|
python-dateutil
|
||||||
python-magic
|
python-magic
|
||||||
pyyaml
|
pyyaml
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -68,6 +68,7 @@ setup(
|
||||||
'frozendict',
|
'frozendict',
|
||||||
'marshmallow',
|
'marshmallow',
|
||||||
'marshmallow_dataclass',
|
'marshmallow_dataclass',
|
||||||
|
'psutil',
|
||||||
'python-dateutil',
|
'python-dateutil',
|
||||||
'python-magic',
|
'python-magic',
|
||||||
'pyyaml',
|
'pyyaml',
|
||||||
|
@ -224,7 +225,7 @@ setup(
|
||||||
# Support for Graphite integration
|
# Support for Graphite integration
|
||||||
'graphite': ['graphyte'],
|
'graphite': ['graphyte'],
|
||||||
# Support for CPU and memory monitoring and info
|
# Support for CPU and memory monitoring and info
|
||||||
'sys': ['py-cpuinfo', 'psutil'],
|
'sys': ['py-cpuinfo'],
|
||||||
# Support for nmap integration
|
# Support for nmap integration
|
||||||
'nmap': ['python-nmap'],
|
'nmap': ['python-nmap'],
|
||||||
# Support for zigbee2mqtt
|
# Support for zigbee2mqtt
|
||||||
|
|
Loading…
Reference in a new issue