changeset 1247:a6c7f07f1e4d

tasks: implicit tasks + Brython task: - implicit tasks are tasks launched for every site. - a first one is made for Brython: it will install Brython and copy suitable files and set template data if Brython code is written in page `_browser` subdirectory.
author Goffi <goffi@goffi.org>
date Sun, 26 Apr 2020 22:07:18 +0200
parents aaf28d45ae67
children 9b865f2604a9
files libervia/server/tasks/implicit/__init__.py libervia/server/tasks/implicit/task_brython.py libervia/server/tasks/manager.py
diffstat 2 files changed, 127 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/server/tasks/implicit/task_brython.py	Sun Apr 26 22:07:18 2020 +0200
@@ -0,0 +1,95 @@
+#!/ur/bin/env python3
+
+import shutil
+import json
+from pathlib import Path
+from ast import literal_eval
+from sat.core.i18n import _
+from sat.core.log import getLogger
+from sat.core import exceptions
+from sat.tools.common.template import safe
+from libervia.server.constants import Const as C
+from libervia.server.tasks import task
+
+
+log = getLogger(__name__)
+
+
+class Task(task.Task):
+    DOC_DIRS_DEFAULT = ('doc', 'docs')
+
+    def prepare(self):
+        if "brython" not in self.resource.browser_modules:
+            raise exceptions.CancelError(f"No brython module found")
+
+        brython_js = self.build_path / "brython.js"
+        if not brython_js.is_file():
+            installed_ver = None
+        else:
+            with brython_js.open() as f:
+                for line in f:
+                    if line.startswith('// implementation ['):
+                        installed_ver = literal_eval(line[18:])[:3]
+                        log.debug(
+                            f"brython v{'.'.join(str(v) for v in installed_ver)} already "
+                            f"installed")
+                        break
+                else:
+                    log.warning(
+                        f"brython file at {brython_js} doesn't has implementation "
+                        f"version"
+                    )
+                    installed_ver = None
+
+        import brython
+        ver = [int(v) for v in brython.implementation.split('.')[:3]]
+        if ver != installed_ver:
+            log.info(_("Installing Brython v{version}").format(
+                version='.'.join(str(v) for v in ver)))
+            data_path = Path(brython.__file__).parent / 'data'
+            # shutil has blocking method, but the task is run before we start
+            # the web server, so it's not a big deal
+            shutil.copyfile(data_path / "brython.js", brython_js)
+        else:
+            log.debug("Brython is already installed")
+
+        self.WATCH_DIRS = []
+
+        for dyn_data in self.resource.browser_modules["brython"]:
+            url_hash = dyn_data['url_hash']
+            import_url = f"/{C.BUILD_DIR}/{C.BUILD_DIR_DYN}/{url_hash}"
+            on_load_opts = {
+                "debug": 1,
+                "pythonpath": [import_url],
+            }
+            dyn_data['template'] = {
+                "scripts": [{"src": f"/{C.BUILD_DIR}/brython.js"}],
+                "body_onload": f"brython({json.dumps(on_load_opts)})",
+            }
+            self.WATCH_DIRS.append(dyn_data['path'].resolve())
+
+    def start(self):
+        dyn_path = self.build_path / C.BUILD_DIR_DYN
+        for dyn_data in self.resource.browser_modules["brython"]:
+            url_hash = dyn_data['url_hash']
+            page_dyn_path = dyn_path / url_hash
+            if page_dyn_path.exists():
+                log.debug("cleaning existing path")
+                shutil.rmtree(page_dyn_path)
+
+            page_dyn_path.mkdir(parents=True, exist_ok=True)
+            log.debug("copying browser python files")
+            for p in dyn_data['path'].iterdir():
+                log.debug(f"copying {p}")
+                if p.is_dir():
+                    shutil.copytree(p, page_dyn_path)
+                else:
+                    shutil.copy(p, page_dyn_path)
+
+            script = {
+                'type': 'text/python',
+                'src': f"/{C.BUILD_DIR}/{C.BUILD_DIR_DYN}/{url_hash}/__init__.py"
+            }
+            scripts = dyn_data['template']['scripts']
+            if script not in scripts:
+                scripts.append(script)
--- a/libervia/server/tasks/manager.py	Sun Apr 26 22:01:13 2020 +0200
+++ b/libervia/server/tasks/manager.py	Sun Apr 26 22:07:18 2020 +0200
@@ -17,19 +17,21 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import os
 import os.path
+from pathlib import Path
 import importlib.util
+from twisted.internet import defer
 from sat.core.log import getLogger
 from sat.core import exceptions
 from sat.core.i18n import _
 from sat.tools import utils
 from libervia.server.constants import Const as C
+from . import implicit
 
 log = getLogger(__name__)
 
 
 class TasksManager:
     """Handle tasks of a Libervia site"""
-    FILE_EXTS = {'py'}
 
     def __init__(self, host, site_resource):
         """
@@ -37,14 +39,14 @@
         """
         self.host = host
         self.resource = site_resource
-        self.tasks_dir = os.path.join(self.resource.site_path, C.TASKS_DIR)
+        self.tasks_dir = self.site_path / C.TASKS_DIR
         self.tasks = {}
         self._build_path = None
         self._current_task = None
 
     @property
     def site_path(self):
-        return self.resource.site_path
+        return Path(self.resource.site_path)
 
     @property
     def build_path(self):
@@ -77,35 +79,26 @@
                     raise ValueError(_("Unexpected value for {var}: {value!r}").format(
                         var=var, value=value))
 
-    async 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):
+    async def parseTasksDir(self, dir_path):
+        log.debug(f"parsing tasks in {dir_path}")
+        tasks_paths = sorted(dir_path.glob('task_*.py'))
+        for task_path in tasks_paths:
+            if not task_path.is_file():
                 continue
-            task_name, ext = os.path.splitext(filename)
-            task_name = task_name[5:].lower().strip()
+            task_name = task_path.stem[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))
+            log.debug(f"task {task_name} found")
             module_name = f"{self.site_name}.task.{task_name}"
 
-            spec = importlib.util.spec_from_file_location(module_name, filepath)
+            spec = importlib.util.spec_from_file_location(module_name, task_path)
             task_module = importlib.util.module_from_spec(spec)
             spec.loader.exec_module(task_module)
             task = task_module.Task(self)
-            self.tasks[task_name] = task
-
 
             # we launch prepare, which is a method used to prepare
             # data at runtime (e.g. set WATCH_DIRS using config)
@@ -114,7 +107,13 @@
             except AttributeError:
                 pass
             else:
-                await utils.asDeferred(prepare)
+                try:
+                    await utils.asDeferred(prepare)
+                except exceptions.CancelError as e:
+                    log.debug(f"Skipping {task_name} which cancelled itself: {e}")
+                    continue
+
+            self.tasks[task_name] = task
             self.validateData(task)
             if self.host.options['dev_mode']:
                 dirs = task.WATCH_DIRS or []
@@ -123,6 +122,18 @@
                         dir_, auto_add=True, recursive=True,
                         callback=self._autorunTask, task_name=task_name)
 
+    async def parseTasks(self):
+        # implicit tasks are always run
+        implicit_path = Path(implicit.__file__).parent
+        await self.parseTasksDir(implicit_path)
+        # now we check if there are tasks specific to this site
+        if not self.tasks_dir.is_dir():
+            log.debug(_("{name} has no task to launch.").format(
+                name = self.resource.site_name or "default site"))
+            return
+        else:
+            await self.parseTasksDir(self.tasks_dir)
+
     def _autorunTask(self, host, filepath, flags, task_name):
         """Called when an event is received from a watched directory"""
         if flags == ['create']: