view libervia/server/tasks.py @ 1216:b2d067339de3

python 3 port: /!\ Python 3.6+ is now needed to use libervia /!\ instability may occur and features may not be working anymore, this will improve with time /!\ TxJSONRPC dependency has been removed The same procedure as in backend has been applied (check backend commit ab2696e34d29 logs for details). Removed now deprecated code (Pyjamas compiled browser part, legacy blog, JSON RPC related code). Adapted code to work without `html` and `themes` dirs.
author Goffi <goffi@goffi.org>
date Tue, 13 Aug 2019 19:12:31 +0200
parents 170802865156
children 987595a254b0
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

# Libervia: a Salut à Toi frontend
# Copyright (C) 2011-2019 Jérôme Poisson <goffi@goffi.org>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import os
import os.path
from twisted.internet import defer
from twisted.python.procutils import which
from sat.core import exceptions
from sat.core.i18n import _
from libervia.server.constants import Const as C
from collections import OrderedDict
from sat.core.log import getLogger
from sat.tools.common import async_process

log = getLogger(__name__)


class TasksManager(object):
    """Handle tasks of a Libervia site"""
    FILE_EXTS = {'py'}

    def __init__(self, host, site_resource):
        """
        @param site_resource(LiberviaRootResource): root resource of the site to manage
        """
        self.host = host
        self.resource = site_resource
        self.tasks_dir = os.path.join(self.resource.site_path, C.TASKS_DIR)
        self.tasks = OrderedDict()
        self.parseTasks()
        self._build_path = None
        self._current_task = None

    @property
    def site_path(self):
        return self.resource.site_path

    @property
    def build_path(self):
        """path where generated files will be build for this site"""
        if self._build_path is None:
            self._build_path = self.host.getBuildPath(self.site_name)
        return self._build_path

    def getConfig(self, key, default=None, value_type=None):
        return self.host.getConfig(self.resource, key=key, default=default,
                                   value_type=value_type)

    @property
    def site_name(self):
        return self.resource.site_name

    @property
    def task_data(self):
        return self.tasks[self._current_task]['data']

    def validateData(self, data):
        """Check values in data"""

        for var, default, allowed in (("ON_ERROR", "stop", ("continue", "stop")),
                                      ("LOG_OUTPUT", True, bool),
                                      ("WATCH_DIRS", [], list)):
            value = data.setdefault(var, default)
            if isinstance(allowed, type):
                if not isinstance(value, allowed):
                    raise ValueError(
                        _("Unexpected value for {var}, {allowed} is expected.")
                        .format(var = var, allowed = allowed))
            else:
                if not value in allowed:
                    raise ValueError(_("Unexpected value for {var}: {value}").format(
                        var = var, value = value))

        for var, default, allowed in [["ON_ERROR", "stop", ("continue", "stop")]]:
            value = data.setdefault(var, default)
            if not value in allowed:
                raise ValueError(_("Unexpected value for {var}: {value}").format(
                    var = var, value = value))

    def parseTasks(self):
        if not os.path.isdir(self.tasks_dir):
            log.debug(_("{name} has no task to launch.").format(
                name = self.resource.site_name or "default site"))
            return
        filenames = os.listdir(self.tasks_dir)
        filenames.sort()
        for filename in filenames:
            filepath = os.path.join(self.tasks_dir, filename)
            if not filename.startswith('task_') or not os.path.isfile(filepath):
                continue
            task_name, ext = os.path.splitext(filename)
            task_name = task_name[5:].lower().strip()
            if not task_name:
                continue
            if ext[1:] not in self.FILE_EXTS:
                continue
            if task_name in self.tasks:
                raise exceptions.ConflictError(
                    "A task with the name [{name}] already exists".format(
                        name=task_name))
            task_data = {"__name__": "{site_name}.task.{name}".format(
                site_name=self.site_name, name=task_name)}
            self.tasks[task_name] = {
                'path': filepath,
                'data': task_data,
            }
            exec(compile(open(filepath, "rb").read(), filepath, 'exec'), task_data)
            # we launch prepare, which is a method used to prepare
            # data at runtime (e.g. set WATCH_DIRS using config)
            try:
                prepare = task_data['prepare']
            except KeyError:
                pass
            else:
                prepare(self)
            self.validateData(task_data)
            if self.host.options['dev_mode']:
                dirs = task_data.get('WATCH_DIRS', [])
                for dir_ in dirs:
                    self.host.files_watcher.watchDir(
                        dir_, auto_add=True, recursive=True,
                        callback=self._autorunTask, task_name=task_name)

    def _autorunTask(self, host, filepath, flags, task_name):
        """Called when an event is received from a watched directory"""
        if flags == ['create']:
            return
        return self.runTask(task_name)

    @defer.inlineCallbacks
    def runTask(self, task_name):
        """Run a single task

        @param task_name(unicode): name of the task to run
        """
        task_value = self.tasks[task_name]
        self._current_task = task_name
        log.info(_('== running task "{task_name}" for {site_name} =='.format(
            task_name=task_name, site_name=self.site_name)))
        data = task_value['data']
        os.chdir(self.site_path)
        try:
            yield data['start'](self)
        except Exception as e:
            on_error = data['ON_ERROR']
            if on_error == 'stop':
                raise e
            elif on_error == 'continue':
                log.warning(_('Task "{task_name}" failed for {site_name}: {reason}')
                    .format(task_name=task_name, site_name=self.site_name, reason=e))
            else:
                raise exceptions.InternalError("we should never reach this point")
        self._current_task = None

    @defer.inlineCallbacks
    def runTasks(self):
        """Run all the tasks found"""
        old_path = os.getcwd()
        for task_name, task_value in self.tasks.items():
            yield self.runTask(task_name)
        os.chdir(old_path)

    def findCommand(self, name, *args):
        """Find full path of a shell command

        @param name(unicode): name of the command to find
        @param *args(unicode): extra names the command may have
        @return (unicode): full path of the command
        @raise exceptions.NotFound: can't find this command
        """
        names = (name,) + args
        for n in names:
            try:
                cmd_path = which(n)[0]
            except IndexError:
                pass
            else:
                return cmd_path
        raise exceptions.NotFound(_(
            "Can't find {name} command, did you install it?").format(name=name))

    def runCommand(self, command, *args, **kwargs):
        kwargs['verbose'] = self.task_data["LOG_OUTPUT"]
        return async_process.CommandProtocol.run(command, *args, **kwargs)