source: ogAgent-Git/src/opengnsys/workers/oglive_worker.py @ 4d34c5d

browser-nuevodecorare-oglive-methodsexec-ogbrowserfix-urllog-sess-lenmainno-tlsogagentuser-sigtermoggitoggit-notlsoglogoglog2ping1ping2ping3ping4sched-tasktlstls-again 3.0.0
Last change on this file since 4d34c5d was c9d6a54, checked in by Natalia Serrano <natalia.serrano@…>, 2 months ago

refs #1760 make cfg2obj more robust

  • Property mode set to 100644
File size: 22.7 KB
RevLine 
[a67669b]1# -*- coding: utf-8 -*-
2#
3# Copyright (c) 2024 Qindel Formación y Servicios S.L.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without modification,
7# are permitted provided that the following conditions are met:
8#
9#    * Redistributions of source code must retain the above copyright notice,
10#      this list of conditions and the following disclaimer.
11#    * Redistributions in binary form must reproduce the above copyright notice,
12#      this list of conditions and the following disclaimer in the documentation
13#      and/or other materials provided with the distribution.
14#    * Neither the name of Virtual Cable S.L. nor the names of its contributors
15#      may be used to endorse or promote products derived from this software
16#      without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28"""
29@author: Natalia Serrano, nserrano at qindel dot com
30"""
31# pylint: disable=unused-wildcard-import,wildcard-import
32
33import os
[f8d6706]34import re
[647489d]35import time
36import random
[a67669b]37import subprocess
[9b91eed]38import threading
[87a5258]39import signal
[a67669b]40
41from configparser import NoOptionError
42from opengnsys import REST
43from opengnsys.log import logger
44from .server_worker import ServerWorker
45
[9b91eed]46## https://stackoverflow.com/questions/6893968/how-to-get-the-return-value-from-a-thread
47class ThreadWithResult (threading.Thread):
48    def run (self):
49        try:
50            self.result = None
51            if self._target is not None:
[69be238]52                ## the first arg in self._args is the queue
[f8d6706]53                self.pid_q    = self._args[0]
54                self.stdout_q = self._args[1]
55                self._args    = self._args[2:]
[9b91eed]56                try:
57                    self.result = self._target (*self._args, **self._kwargs)
58                except Exception as e:
[74ef2b7]59                    self.result = { 'res': 2, 'der': f'got exception: ({e})' }    ## res=2 as defined in ogAdmClient.c:2048
[9b91eed]60        finally:
61            # Avoid a refcycle if the thread is running a function with an argument that has a member that points to the thread.
[f8d6706]62            del self._target, self._args, self._kwargs, self.pid_q, self.stdout_q
[9b91eed]63
[a67669b]64class ogLiveWorker(ServerWorker):
[9b91eed]65    thread_list = {}
[87a5258]66    thread_lock = threading.Lock()
[9b91eed]67
[3a9c293]68    tbErroresScripts = [
69        "Se han generado errores desconocidos. No se puede continuar la ejecución de este módulo",        ## 0
70                "001-Formato de ejecución incorrecto.",
71                "002-Fichero o dispositivo no encontrado",
72                "003-Error en partición de disco",
73                "004-Partición o fichero bloqueado",
74                "005-Error al crear o restaurar una imagen",
75                "006-Sin sistema operativo",
76                "007-Programa o función BOOLEAN no ejecutable",
77                "008-Error en la creación del archivo de eco para consola remota",
78                "009-Error en la lectura del archivo temporal de intercambio",
79                "010-Error al ejecutar la llamada a la interface de administración",
80                "011-La información retornada por la interface de administración excede de la longitud permitida",
81                "012-Error en el envío de fichero por la red",
82                "013-Error en la creación del proceso hijo",
83                "014-Error de escritura en destino",
84                "015-Sin Cache en el Cliente",
85                "016-No hay espacio en la cache para almacenar fichero-imagen",
86                "017-Error al Reducir el Sistema Archivos",
87                "018-Error al Expandir el Sistema Archivos",
88                "019-Valor fuera de rango o no válido.",
89                "020-Sistema de archivos desconocido o no se puede montar",
90                "021-Error en partición de caché local",
91                "022-El disco indicado no contiene una particion GPT",
92                "023-Error no definido",
93                "024-Error no definido",
94                "025-Error no definido",
95                "026-Error no definido",
96                "027-Error no definido",
97                "028-Error no definido",
98                "029-Error no definido",
99                "030-Error al restaurar imagen - Imagen mas grande que particion",
100                "031-Error al realizar el comando updateCache",
101                "032-Error al formatear",
102                "033-Archivo de imagen corrupto o de otra versión de partclone",
103                "034-Error no definido",
104                "035-Error no definido",
105                "036-Error no definido",
106                "037-Error no definido",
107                "038-Error no definido",
108                "039-Error no definido",
109                "040-Error imprevisto no definido",
110                "041-Error no definido",
111                "042-Error no definido",
112                "043-Error no definido",
113                "044-Error no definido",
114                "045-Error no definido",
115                "046-Error no definido",
116                "047-Error no definido",
117                "048-Error no definido",
118                "049-Error no definido",
119                "050-Error en la generación de sintaxis de transferenica unicast",
120                "051-Error en envio UNICAST de una particion",
121                "052-Error en envio UNICAST de un fichero",
122                "053-Error en la recepcion UNICAST de una particion",
123                "054-Error en la recepcion UNICAST de un fichero",
124                "055-Error en la generacion de sintaxis de transferenica Multicast",
125                "056-Error en envio MULTICAST de un fichero",
126                "057-Error en la recepcion MULTICAST de un fichero",
127                "058-Error en envio MULTICAST de una particion",
128                "059-Error en la recepcion MULTICAST de una particion",
129                "060-Error en la conexion de una sesion UNICAST|MULTICAST con el MASTER",
130                "061-Error no definido",
131                "062-Error no definido",
132                "063-Error no definido",
133                "064-Error no definido",
134                "065-Error no definido",
135                "066-Error no definido",
136                "067-Error no definido",
137                "068-Error no definido",
138                "069-Error no definido",
139                "070-Error al montar una imagen sincronizada.",
140                "071-Imagen no sincronizable (es monolitica).",
141                "072-Error al desmontar la imagen.",
142                "073-No se detectan diferencias entre la imagen basica y la particion.",
143                "074-Error al sincronizar, puede afectar la creacion/restauracion de la imagen.",
144                "Error desconocido",
145        ]
146
[6163c0b]147    def notifier (self, job_id, result):
148        result['job_id'] = job_id
[9ac107d]149        self.REST.sendMessage ('clients/status/webhook', result)
[def6750]150
[87a5258]151    def killer (self, job_id):
152        logger.debug (f'killer() called, job_id ({job_id})')
153        if job_id not in self.thread_list: return { 'res': 2, 'der': 'Unknown job' }
154
155        with self.thread_lock:
156            if 'thread' not in self.thread_list[job_id]: return { 'res': 2, 'der': 'Job is not running' }
[69be238]157            t   = self.thread_list[job_id]['thread']
[87a5258]158            pid = self.thread_list[job_id]['child_pid']
159            logger.debug (f'pid ({pid})')
160            try_times = 8
161            sig = signal.SIGTERM
[69be238]162            msg = f'could not kill pid ({pid}) after ({try_times}) tries'
163            success = 2    ## mimic cmd['res'] in respuestaEjecucionComando(): "1" means success, "2" means failed
[87a5258]164            while True:
165                t.join (0.05)
166                if not t.is_alive():
[69be238]167                    msg = 'job terminated'
168                    success = 1
169                    logger.debug (msg)
[87a5258]170                    self.thread_list[job_id]['child_pid'] = None
171                    break
[69be238]172                ## race condition: if the subprocess finishes just here, then we already checked that t.is_alive() is true, but os.path.exists(/proc/pid) will be false below. msg will be 'nothing to kill'.
173                ## this is fine in the first iteration of the loop, before we send any signals. In the rest of iterations, after some signals were sent, msg should be 'job terminated' instead.
[87a5258]174                if pid:
175                    if os.path.exists (f'/proc/{pid}'):
[69be238]176                        logger.debug (f'sending signal ({sig}) to pid ({pid})')
177                        ## if the process finishes just here, nothing happens: the signal is sent to the void
178                        os.kill (pid, sig)
179                        #subprocess.run (['kill', '--signal', str(sig), str(pid)])
[87a5258]180                    else:
[69be238]181                        msg = f'pid ({pid}) is gone, nothing to kill'
182                        success = 1
183                        logger.debug (msg)
[87a5258]184                        self.thread_list[job_id]['child_pid'] = None
[69be238]185                        break
[87a5258]186                else:
[69be238]187                    msg = 'no PID to kill'
188                    logger.debug (msg)
189
[87a5258]190                if not try_times: break
191                if 4 == try_times: sig = signal.SIGKILL   ## change signal after a few tries
192                try_times -= 1
193                time.sleep (0.4)
194
[69be238]195        return { 'res':success, 'der':msg }
[87a5258]196
[f8d6706]197    def _extract_progress (self, job_id, ary=[]):
198        progress = None
199        for i in ary:
[1de04f3]200            if m := re.search (r'^\[([0-9]+)\]', i):     ## look for strings like '[10]', '[60]'
201                logger.debug (f"matched regex, m.groups ({m.groups()})")
202                progress = float (m.groups()[0]) / 100
[f8d6706]203        return progress
204
[def6750]205    def mon (self):
206        while True:
[87a5258]207            with self.thread_lock:
208                for k in self.thread_list:
209                    elem = self.thread_list[k]
210                    if 'thread' not in elem: continue
211                    logger.debug (f'considering thread ({k})')
212
[f8d6706]213                    if self.pid_q:
214                        if not self.pid_q.empty():
215                            elem['child_pid'] = self.pid_q.get()
[87a5258]216                            logger.debug (f'queue not empty, got pid ({elem["child_pid"]})')
[f8d6706]217
218                    if self.stdout_q:
219                        partial = ''
220                        while not self.stdout_q.empty():
221                            partial += self.stdout_q.get()
222                        lines = partial.splitlines()
223                        if len (lines):
224                            p = self._extract_progress (k, lines)
225                            if p:
226                                m = { "job_id": k, "progress": p }
[d5275c6]227                                self.REST.sendMessage ('clients/status/webhook', { "job_id": k, "progress": p })
[87a5258]228
229                    elem['thread'].join (0.05)
230                    if not elem['thread'].is_alive():
231                        logger.debug (f'is no longer alive, k ({k}) thread ({elem["thread"]})')
232                        elem['running'] = False
233                        elem['result'] = elem['thread'].result
234                        del elem['thread']
235                        self.notifier (k, elem['result'])
[def6750]236
237            time.sleep (1)
238
[a67669b]239    def interfaceAdmin (self, method, parametros=[]):
[c218eff]240        if method in ['Apagar', 'CambiarAcceso', 'Configurar', 'CrearImagen', 'EjecutarScript', 'getConfiguration', 'getIpAddress', 'IniciarSesion', 'InventarioHardware', 'InventarioSoftware', 'Reiniciar', 'RestaurarImagen']:
241            ## python
242            logger.debug (f'({method}) is a python method')
243            exe = '{}/{}.py'.format (self.pathinterface, method)
244            proc = [exe]+parametros
245        else:  ## ConsolaRemota procesaCache
246            ## bash
247            logger.debug (f'({method}) is a bash method')
248            exe = '{}/{}'.format (self.pathinterface, method)
[a67669b]249
[c218eff]250            LANG = os.environ.get ('LANG', 'en_GB.UTF-8').replace ('UTF_8', 'UTF-8')
251            devel_bash_prefix = f'''
252                PATH=/opt/opengnsys/scripts/:$PATH;
253                source /opt/opengnsys/etc/lang.{LANG}.conf;
254                for I in /opt/opengnsys/lib/engine/bin/*.lib; do source $I; done;
255                for i in $(declare -F |cut -f3 -d" "); do export -f $i; done;
256            '''
257
258            if parametros:
259                proc = ['bash', '-c', '{} {} {}'.format (devel_bash_prefix, exe, ' '.join (parametros))]
260            else:
261                proc = ['bash', '-c', '{} {}'.format (devel_bash_prefix, exe)]
[87a5258]262
[c218eff]263        logger.debug ('subprocess.run ("{}")'.format (' '.join (proc)))
[87a5258]264        p = subprocess.Popen (proc, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
[f8d6706]265        if self.pid_q:
266            self.pid_q.put (p.pid)
[69be238]267        else:
268            ## esto sucede por ejemplo cuando arranca el agente, que estamos en interfaceAdmin() en el mismo hilo, sin _long_running_job ni hilo separado
269            logger.debug ('no queue--not writing any PID to it')
[f8d6706]270
[87a5258]271        sout = serr = ''
272        while p.poll() is None:
[f8d6706]273            for l in iter (p.stdout.readline, b''):
274                partial = l.decode ('utf-8', 'ignore')
275                if self.stdout_q: self.stdout_q.put (partial)
276                sout += partial
277            for l in iter (p.stderr.readline, b''):
278                partial = l.decode ('utf-8', 'ignore')
279                serr += partial
[87a5258]280            time.sleep (1)
281        sout = sout.strip()
282        serr = serr.strip()
283
[a67669b]284        ## DEBUG
285        logger.info (f'stdout follows:')
[87a5258]286        for l in sout.splitlines():
[a67669b]287            logger.info (f'  {l}')
288        logger.info (f'stderr follows:')
[87a5258]289        for l in serr.splitlines():
[a67669b]290            logger.info (f'  {l}')
291        ## /DEBUG
292        if 0 != p.returncode:
293            cmd_txt = ' '.join (proc)
294            logger.error (f'command ({cmd_txt}) failed, stderr follows:')
[87a5258]295            for l in serr.splitlines():
[a67669b]296                logger.error (f'  {l}')
297            raise Exception (f'command ({cmd_txt}) failed, see log for details')
[87a5258]298        return sout
[a67669b]299
300    def tomaIPlocal (self):
301        try:
302            self.IPlocal = self.interfaceAdmin ('getIpAddress')
303        except Exception as e:
304            logger.error (e)
305            logger.error ('No se ha podido recuperar la dirección IP del cliente')
306            return False
307        logger.info ('local IP is "{}"'.format (self.IPlocal))
308        return True
309
[376dec4]310    def tomaMAClocal (self):
311        ## tomaIPlocal() calls interfaceAdm('getIpAddress')
312        ## getIpAddress runs 'ip addr show' and returns the IP address of every network interface except "lo"
313        ## (ie. breaks badly if there's more than one network interface)
314        ## let's make the same assumptions here
315        mac = subprocess.run (["ip -json link show |jq -r '.[] |select (.ifname != \"lo\") |.address'"], shell=True, capture_output=True, text=True)
316        self.mac = mac.stdout.strip()
317        return True
318
[a67669b]319    def enviaMensajeServidor (self, path, obj={}):
320        obj['iph'] = self.IPlocal          ## Ip del ordenador
321        obj['ido'] = self.idordenador      ## Identificador del ordenador
322        obj['npc'] = self.nombreordenador  ## Nombre del ordenador
323        obj['idc'] = self.idcentro         ## Identificador del centro
324        obj['ida'] = self.idaula           ## Identificador del aula
325
326        res = self.REST.sendMessage ('/'.join ([self.name, path]), obj)
327
328        if (type (res) is not dict):
329            #logger.error ('No se ha podido establecer conexión con el Servidor de Administración')   ## Error de conexión con el servidor
330            logger.debug (f'res ({res})')
331            logger.error ('Error al enviar trama ***send() fallo')
332            return False
333
334        return res
335
[e28094e]336    ## en C, esto envia una trama de respuesta al servidor. Devuelve un boolean
337    ## en python, simplemente termina de construir la respuesta y la devuelve; no envía nada por la red. El caller la usa en return() para enviar implícitamente la respuesta
338    def respuestaEjecucionComando (self, cmd, herror, ids=None):
339        if ids:                 ## Existe seguimiento
340            cmd['ids'] = ids    ## Añade identificador de la sesión
341
342        if 0 == herror:  ## el comando terminó con resultado satisfactorio
343            cmd['res'] = 1
344            cmd['der'] = ''
345        else:            ## el comando tuvo algún error
346            cmd['res'] = 2
[69be238]347            cmd['der'] = self.tbErroresScripts[herror]
[e28094e]348
349        return cmd
350
[a67669b]351    def cargaPaginaWeb (self, url=None):
352        if (not url): url = self.urlMenu
[74a6937]353        os.system ('pkill -9 browser')
[a67669b]354
[74a6937]355        p = subprocess.Popen (['/usr/bin/browser', '-qws', url])
[a67669b]356        try:
357            p.wait (2)       ## if the process dies before 2 seconds...
358            logger.error ('Error al ejecutar la llamada a la interface de administración')
359            logger.error ('Error en la creación del proceso hijo')
360            logger.error ('return code "{}"'.format (p.returncode))
361            return False
362        except subprocess.TimeoutExpired:
363            pass
364
365        return True
366
367    def muestraMenu (self):
368        self.cargaPaginaWeb()
369
370    def muestraMensaje (self, idx):
371        self.cargaPaginaWeb (f'{self.urlMsg}?idx={idx}')
372
373    def LeeConfiguracion (self):
374        try:
375            parametroscfg = self.interfaceAdmin ('getConfiguration')   ## Configuración de los Sistemas Operativos del cliente
376        except Exception as e:
377            logger.error (e)
378            logger.error ('No se ha podido recuperar la dirección IP del cliente')
379            return None
380        logger.debug ('parametroscfg ({})'.format (parametroscfg))
381        return parametroscfg
382
[a2df6af]383    def cfg2obj (self, cfg):
384        obj = []
385        ptrPar = cfg.split ('\n')
386        for line in ptrPar:
387            elem = {}
388            ptrCfg = line.split ('\t')
389
[0f5cf07]390            for item in ptrCfg:
[c9d6a54]391                if '=' not in item:
392                    logger.warning (f'invalid item ({item})')
393                    continue
394                k, v = item.split ('=', maxsplit=1)
[0f5cf07]395                elem[k] = v
[a2df6af]396
397            obj.append (elem)
398
399        return obj
400
[f8563ec]401    def onActivation (self):
[a67669b]402        if not os.path.exists ('/scripts/oginit'):
403            ## no estamos en oglive, este modulo no debe cargarse
404            ## esta lógica la saco de src/opengnsys/linux/operations.py, donde hay un if similar
405            raise Exception ('Refusing to load within an operating system')
406
407        self.pathinterface   = None
408        self.IPlocal         = None     ## Ip del ordenador
[376dec4]409        self.mac             = None     ## MAC del ordenador
[a67669b]410        self.idordenador     = None     ## Identificador del ordenador
411        self.nombreordenador = None     ## Nombre del ordenador
412        self.cache           = None
413        self.idproautoexec   = None
414        self.idcentro        = None     ## Identificador del centro
415        self.idaula          = None     ## Identificador del aula
[f8d6706]416        self.pid_q           = None     ## for passing PIDs around
417        self.stdout_q        = None     ## for passing stdout
418        self.progress_jobs   = {}
[a67669b]419
[2f4ade7]420        ogcore_scheme   = os.environ.get ('OGAGENTCFG_OGCORE_SCHEME',  'https')
421        ogcore_ip       = os.environ.get ('OGAGENTCFG_OGCORE_IP',      '192.168.2.1')
422        ogcore_port     = os.environ.get ('OGAGENTCFG_OGCORE_PORT',    '8443')
423        urlmenu_scheme  = os.environ.get ('OGAGENTCFG_URLMENU_SCHEME', 'https')
424        urlmenu_ip      = os.environ.get ('OGAGENTCFG_URLMENU_IP',     '192.168.2.1')
425        urlmenu_port    = os.environ.get ('OGAGENTCFG_URLMENU_PORT',   '8443')
[9ac107d]426        ogcore_ip_port  = ':'.join (map (str, filter (None, [ogcore_ip,  ogcore_port ])))
427        urlmenu_ip_port = ':'.join (map (str, filter (None, [urlmenu_ip, urlmenu_port])))
[a67669b]428        try:
429            url                = self.service.config.get (self.name, 'remote')
430            loglevel           = self.service.config.get (self.name, 'log')
431            self.pathinterface = self.service.config.get (self.name, 'pathinterface')
432            self.urlMenu       = self.service.config.get (self.name, 'urlMenu')
433            self.urlMsg        = self.service.config.get (self.name, 'urlMsg')
[9ac107d]434
435            url          = url.format          (ogcore_scheme,  ogcore_ip_port)
436            self.urlMenu = self.urlMenu.format (urlmenu_scheme, urlmenu_ip_port)
[a67669b]437        except NoOptionError as e:
438            logger.error ("Configuration error: {}".format (e))
439            raise e
440        logger.setLevel (loglevel)
441        self.REST = REST (url)
442
443        if not self.tomaIPlocal():
444            raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
[376dec4]445
446        if not self.tomaMAClocal():
447            raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo')
[647489d]448
[f8563ec]449        threading.Thread (name='monitoring_thread', target=self.mon, daemon=True).start()
[068e0cf]450
[def6750]451    def _long_running_job (self, name, f, args):
[647489d]452        any_job_running = False
453        for k in self.thread_list:
454            if self.thread_list[k]['running']:
455                any_job_running = True
456                break
457        if any_job_running:
458            logger.info ('some job is already running, refusing to launch another one')
459            return { 'job_id': None, 'message': 'some job is already running, refusing to launch another one' }
460
461        job_id = '{}-{}'.format (name, ''.join (random.choice ('0123456789abcdef') for _ in range (8)))
[87a5258]462        import queue
[f8d6706]463        self.pid_q    = queue.Queue()   ## a single queue works for us because we never have more than one long_running_job at the same time
464        self.stdout_q = queue.Queue()
[647489d]465        self.thread_list[job_id] = {
[f8d6706]466            'thread': ThreadWithResult (target=f, args=(self.pid_q, self.stdout_q) + args),
[647489d]467            'starttime': time.time(),
[87a5258]468            'child_pid': None,
[647489d]469            'running': True,
470            'result': None
471        }
472        self.thread_list[job_id]['thread'].start()
473        return { 'job_id': job_id }
[87a5258]474
475## para matar threads tengo lo siguiente:
[f8d6706]476## - aqui en _long_running_job meto una cola en self.pid_q
477## - (self.pid_q fue inicializado a None al instanciar el objeto, para evitar error "objeto no tiene 'pid_q'")
[87a5258]478## - en el thread_list también tengo un child_pid para almacenar el pid de los procesos hijos
[f8d6706]479## - al crear el ThreadWithResult le paso la cola, y luego en run() la recojo y la meto en el self.pid_q del thread
[69be238]480## - en interfaceAdmin() al hacer subprocess.Popen(), recojo el pid y lo escribo en la queue
[87a5258]481## - en mon() recojo pids de la queue y los meto en thread_list 'child_pid'
[69be238]482##   - algunas funciones llaman a interfaceAdmin más de una vez, y escriben más de un pid en la cola, y en mon() voy recogiendo y actualizando
483##     - por ejemplo EjecutarScript llama a interfaceAdmin() y luego llama a LeeConfiguracion() el cual llama a interfaceAdmin() otra vez
[87a5258]484## - y cuando nos llamen a KillJob, terminamos en killer() el cual coge el 'child_pid' y zas
485##   - pero a lo mejor el child ya terminó
486##   - o a lo mejor el KillJob nos llegó demasiado pronto y todavía no hubo ningún child
487##
488## $ curl --insecure -X POST --data '{"nfn":"EjecutarScript","scp":"cd /usr; sleep 30; pwd; ls","ids":"0"}' https://192.168.2.199:8000/ogAdmClient/EjecutarScript
489## {"job_id": "EjecutarScript-333feb3f"}
[f8d6706]490## $ curl --insecure -X POST --data '{"job_id":"EjecutarScript-333feb3f"}' https://192.168.2.199:8000/ogAdmClient/KillJob
[87a5258]491##
[69be238]492## funciona bien, excepto que el PID no muere xD, ni siquiera haciendo subprocess.run('kill')
[f8d6706]493
494## para mostrar el progreso de los jobs reutilizo la misma infra
495## una cola self.stdout_q
496## en interfaceAdmin escribo la stdout parcial que ya venia recogiendo
497## mon() lo recoge y le hace un POST a ogcore
Note: See TracBrowser for help on using the repository browser.