Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
processes.py900 linesDownload Raw Back to bgprocess
1# -*- coding: utf-8 -*-2##########################################################################3#4# pgAdmin 4 - PostgreSQL Tools5#6# Copyright (C) 2013 - 2024, The pgAdmin Development Team7# This software is released under the PostgreSQL License8#9##########################################################################10 11"""12Introduce a function to run the process executor in detached mode.13"""14import csv15import os16import sys17import psutil18from abc import ABCMeta, abstractmethod19from datetime import datetime, timedelta, timezone20from pickle import dumps, loads21from subprocess import Popen, PIPE22import logging23import json24import shutil25 26from pgadmin.utils import u_encode, file_quote, fs_encoding, \27    get_complete_file_path, get_storage_directory, IS_WIN28from pgadmin.utils.constants import KERBEROS29from pgadmin.utils.locker import ConnectionLocker30from pgadmin.utils.preferences import Preferences31 32from dateutil import parser33from flask import current_app, session34from flask_babel import gettext as _35from flask_security import current_user36 37import config38from pgadmin.model import Process, db39from io import StringIO40 41PROCESS_NOT_STARTED = 042PROCESS_STARTED = 143PROCESS_FINISHED = 244PROCESS_TERMINATED = 345PROCESS_NOT_FOUND = _("Could not find a process with the specified ID.")46 47 48def get_current_time(format='%Y-%m-%d %H:%M:%S.%f %z'):49    """50    Generate the current time string in the given format.51    """52    return datetime.now(timezone.utc).strftime(format)53 54 55class IProcessDesc(metaclass=ABCMeta):56    @property57    @abstractmethod58    def message(self):59        pass60 61    @abstractmethod62    def details(self, cmd, args):63        pass64 65    @property66    def current_storage_dir(self):67 68        if config.SERVER_MODE:69 70            process_file = self.bfile71            try:72                # check if file name is encoded with UTF-873                process_file = self.bfile.decode('utf-8')74            except Exception:75                # do nothing if bfile is not encoded.76                pass77 78            path = get_complete_file_path(process_file)79            path = process_file if path is None else path80 81            if IS_WIN:82                path = os.path.realpath(path)83 84            storage_directory = os.path.basename(get_storage_directory())85 86            if storage_directory in path:87                start = path.index(storage_directory)88                end = start + (len(storage_directory))89                last_dir = os.path.dirname(path[end:])90            else:91                last_dir = process_file92 93            last_dir = replace_path_for_win(last_dir)94 95            return None if hasattr(self, 'is_import') and self.is_import \96                else last_dir97 98        return None99 100 101def replace_path_for_win(last_dir=None):102    if IS_WIN:103        if '\\' in last_dir and len(last_dir) == 1:104            last_dir = last_dir.replace('\\', '\\\\')105        else:106            last_dir = last_dir.replace('\\', '/')107 108    return last_dir109 110 111class BatchProcess:112    def __init__(self, **kwargs):113 114        self.id = self.desc = self.cmd = self.args = self.log_dir = \115            self.stdout = self.stderr = self.stime = self.etime = \116            self.ecode = self.manager_obj = None117        self.env = dict()118 119        if 'id' in kwargs:120            self._retrieve_process(kwargs['id'])121        else:122            _cmd = kwargs['cmd']123            # Get system's interpreter124            if kwargs['cmd'] == 'python':125                _cmd = self._get_python_interpreter()126 127            self._create_process(128                kwargs['desc'], _cmd, kwargs['args']129            )130 131        if 'manager_obj' in kwargs:132            self.manager_obj = kwargs['manager_obj']133 134    def _retrieve_process(self, _id):135        p = Process.query.filter_by(pid=_id, user_id=current_user.id).first()136 137        if p is None:138            raise LookupError(PROCESS_NOT_FOUND)139 140        try:141            tmp_desc = loads(bytes.fromhex(p.desc))142        except Exception:143            tmp_desc = loads(p.desc)144 145        # ID146        self.id = _id147        # Description148        self.desc = tmp_desc149        # Status Acknowledged time150        self.atime = p.acknowledge151        # Command152        self.cmd = p.command153        # Arguments154        self.args = p.arguments155        # Log Directory156        self.log_dir = p.logdir157        # Standard ouput log file158        self.stdout = os.path.join(p.logdir, 'out')159        # Standard error log file160        self.stderr = os.path.join(p.logdir, 'err')161        # Start time162        self.stime = p.start_time163        # End time164        self.etime = p.end_time165        # Exit code166        self.ecode = p.exit_code167        # Process State168        self.process_state = p.process_state169 170    def _create_process(self, _desc, _cmd, _args):171        ctime = get_current_time(format='%y%m%d%H%M%S%f')172        log_dir = os.path.join(173            config.SESSION_DB_PATH, 'process_logs'174        )175 176        def random_number(size):177            import secrets178            import string179 180            return ''.join(181                secrets.choice(182                    string.ascii_uppercase + string.digits183                ) for _ in range(size)184            )185 186        created = False187        size = 0188        uid = ctime189        while not created:190            try:191                uid += random_number(size)192                log_dir = os.path.join(log_dir, uid)193                size += 1194                if not os.path.exists(log_dir):195                    os.makedirs(log_dir, int('700', 8))196                    created = True197            except OSError as oe:198                import errno199                if oe.errno != errno.EEXIST:200                    raise201 202        # ID203        self.id = ctime204        # Description205        self.desc = _desc206        # Status Acknowledged time207        self.atime = None208        # Command209        self.cmd = _cmd210        # Log Directory211        self.log_dir = log_dir212        # Standard ouput log file213        self.stdout = os.path.join(log_dir, 'out')214        # Standard error log file215        self.stderr = os.path.join(log_dir, 'err')216        # Start time217        self.stime = None218        # End time219        self.etime = None220        # Exit code221        self.ecode = None222        # Process State223        self.process_state = PROCESS_NOT_STARTED224 225        # Arguments226        self.args = _args227        args_csv_io = StringIO()228        csv_writer = csv.writer(229            args_csv_io, delimiter=str(','), quoting=csv.QUOTE_MINIMAL230        )231        csv_writer.writerow(_args)232 233        args_val = args_csv_io.getvalue().strip(str('\r\n'))234        tmp_desc = dumps(self.desc).hex()235 236        j = Process(237            pid=int(uid),238            command=_cmd,239            arguments=args_val,240            logdir=log_dir,241            desc=tmp_desc,242            user_id=current_user.id243        )244        db.session.add(j)245        db.session.commit()246 247    def check_start_end_time(self):248        """249        Check start and end time to check process is still executing or not.250        :return:251        """252        if self.stime is not None:253            if self.etime is None:254                raise RuntimeError(_('The process has already been started.'))255            raise RuntimeError(256                _('The process has already finished and cannot be restarted.')257            )258 259    def _get_python_interpreter(self):260        """Get Python Interpreter"""261        if os.name == 'nt':262            paths = os.environ['PATH'].split(os.pathsep)263 264            current_app.logger.info(265                "Process Executor: Operating System Path %s",266                str(paths)267            )268 269            interpreter = self.get_windows_interpreter(paths)270        else:271            interpreter = sys.executable272            if interpreter.endswith('uwsgi'):273                interpreter = interpreter.split('uwsgi',274                                                maxsplit=1)[0] + 'python'275 276        return interpreter if interpreter else 'python'277 278    def start(self, cb=None):279        self.check_start_end_time()280 281        executor = file_quote(os.path.join(282            os.path.dirname(u_encode(__file__)), 'process_executor.py'283        ))284 285        interpreter = self._get_python_interpreter()286 287        cmd = [interpreter, executor, self.cmd]288        cmd.extend(self.args)289 290        current_app.logger.info(291            "Executing the process executor with the arguments: %s",292            str(cmd)293        )294 295        # Acquiring lock while copying the environment from the parent process296        # for the child process297        with ConnectionLocker(_is_kerberos_conn=False):298            # Make a copy of environment, and add new variables to support299            env = os.environ.copy()300 301        env['PROCID'] = self.id302        env['OUTDIR'] = self.log_dir303        env['PGA_BGP_FOREGROUND'] = "1"304        if config.SERVER_MODE and session and \305                session['auth_source_manager']['current_source'] == \306                KERBEROS and 'KRB5CCNAME' in session:307            env['KRB5CCNAME'] = session['KRB5CCNAME']308 309        if self.env:310            env.update(self.env)311 312        current_app.logger.debug(self.env)313 314        if cb is not None:315            cb(env)316        if os.name == 'nt':317            DETACHED_PROCESS = 0x00000008318            from subprocess import CREATE_NEW_PROCESS_GROUP319 320            # We need to redirect the standard input, standard output, and321            # standard error to devnull in order to allow it start in detached322            # mode on323            stdout = os.devnull324            stderr = stdout325            stdin = open(os.devnull, "r")326            stdout = open(stdout, "a")327            stderr = open(stderr, "a")328 329            p = Popen(330                cmd,331                close_fds=False,332                env=env,333                stdout=stdout.fileno(),334                stderr=stderr.fileno(),335                stdin=stdin.fileno(),336                creationflags=(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)337            )338        else:339            # if in debug mode, wait for process to complete and340            # get the stdout and stderr of popen.341            if config.CONSOLE_LOG_LEVEL <= logging.DEBUG:342                p = self.get_process_output(cmd, env)343            else:344                p = Popen(345                    cmd, close_fds=True, stdout=None, stderr=None, stdin=None,346                    start_new_session=True, env=env347                )348 349        self.ecode = p.poll()350 351        # Execution completed immediately.352        # Process executor cannot update the status, if it was not able to353        # start properly.354        if self.ecode is not None and self.ecode != 0:355            # There is no way to find out the error message from this process356            # as standard output, and standard error were redirected to357            # devnull.358            p = Process.query.filter_by(359                pid=self.id, user_id=current_user.id360            ).first()361            p.start_time = p.end_time = get_current_time()362            if not p.exit_code:363                p.exit_code = self.ecode364            p.process_state = PROCESS_FINISHED365            db.session.commit()366        else:367            # Update the process state to "Started"368            p = Process.query.filter_by(369                pid=self.id, user_id=current_user.id370            ).first()371            p.process_state = PROCESS_STARTED372            db.session.commit()373 374    def get_process_output(self, cmd, env):375        """376        :param cmd:377        :param env:378        :return:379        """380        p = Popen(381            cmd, close_fds=True, stdout=PIPE, stderr=PIPE, stdin=None,382            start_new_session=True, env=env383        )384 385        output, errors = p.communicate()386        output = output.decode() \387            if hasattr(output, 'decode') else output388        errors = errors.decode() \389            if hasattr(errors, 'decode') else errors390        current_app.logger.debug(391            'Process Watcher Out:{0}'.format(output))392        current_app.logger.debug(393            'Process Watcher Err:{0}'.format(errors))394 395        return p396 397    def preexec_function(self):398        import signal399        # Detaching from the parent process group400        os.setpgrp()401        # Explicitly ignoring signals in the child process402        signal.signal(signal.SIGINT, signal.SIG_IGN)403 404    def get_windows_interpreter(self, paths):405        """406        Get interpreter.407        :param paths:408        :return:409        """410        paths.insert(0, os.path.join(u_encode(sys.prefix), 'Scripts'))411        paths.insert(0, u_encode(sys.prefix))412 413        interpreter = self.which('pythonw.exe', paths)414        if interpreter is None:415            interpreter = self.which('python.exe', paths)416 417        current_app.logger.info(418            "Process Executor: Interpreter value in path: %s",419            str(interpreter)420        )421        if interpreter is None and current_app.PGADMIN_RUNTIME:422            # We've faced an issue with Windows 2008 R2 (x86) regarding,423            # not honouring the environment variables set under the Qt424            # (e.g. runtime), and also setting PYTHONHOME same as425            # sys.executable (i.e. pgAdmin4.exe).426            #427            # As we know, we're running it under the runtime, we can assume428            # that 'venv' directory will be available outside of 'bin'429            # directory.430            #431            # We would try out luck to find python executable based on that432            # assumptions.433            bin_path = os.path.dirname(sys.executable)434 435            venv = os.path.realpath(436                os.path.join(bin_path, '..\\venv')437            )438 439            interpreter = self.which('pythonw.exe', [venv])440            if interpreter is None:441                interpreter = self.which('python.exe', [venv])442 443            current_app.logger.info(444                "Process Executor: Interpreter value in virtual "445                "environment: %s", str(interpreter)446            )447 448            if interpreter is not None:449                # Our assumptions are proven right.450                # Let's append the 'bin' directory to the PATH environment451                # variable. And, also set PYTHONHOME environment variable452                # to 'venv' directory.453                os.environ['PATH'] = bin_path + ';' + os.environ['PATH']454                os.environ['PYTHONHOME'] = venv455 456        return interpreter457 458    def which(self, program, paths):459        def is_exe(fpath):460            return os.path.exists(fpath) and os.access(fpath, os.X_OK)461 462        for path in paths:463            if not os.path.isdir(path):464                continue465            exe_file = os.path.join(u_encode(path, fs_encoding), program)466            if is_exe(exe_file):467                return file_quote(exe_file)468        return None469 470    def read_log(self, logfile, log, pos, ctime, ecode=None, enc='utf-8'):471        import re472        completed = True473        idx = 0474        c = re.compile(r"(\d+),(.*$)")475 476        # If file is not present then477        if not os.path.isfile(logfile):478            return 0, True479 480        with open(logfile, 'rb') as f:481            eofs = os.path.getsize(logfile)482            f.seek(pos, 0)483            if pos == eofs and ecode is None:484                completed = False485 486            while pos < eofs:487                idx += 1488                line = f.readline()489                line = line.decode(enc, 'replace')490                r = c.split(line)491                if len(r) < 3:492                    # ignore this line493                    pos = f.tell()494                    continue495                if r[1] > ctime:496                    completed = False497                    break498                log.append([r[1], r[2]])499                pos = f.tell()500                if idx >= 1024:501                    completed = False502                    break503                if pos == eofs:504                    if ecode is None:505                        completed = False506                    break507 508        return pos, completed509 510    def update_cloud_details(self):511        """512        Parse the output to get the cloud instance details513        """514        _pid = self.id515 516        _process = Process.query.filter_by(517            user_id=current_user.id, pid=_pid518        ).first()519 520        if _process is None:521            raise LookupError(PROCESS_NOT_FOUND)522 523        ctime = get_current_time(format='%y%m%d%H%M%S%f')524        stdout = []525        stderr = []526        out = 0527        err = 0528        cloud_server_id = 0529        cloud_instance = ''530 531        enc = sys.getdefaultencoding()532        if enc == 'ascii':533            enc = 'utf-8'534 535        out, out_completed = self.read_log(536            self.stdout, stdout, out, ctime, _process.exit_code, enc537        )538        err, err_completed = self.read_log(539            self.stderr, stderr, err, ctime, _process.exit_code, enc540        )541 542        from pgadmin.misc.cloud import update_server, clear_cloud_session543        if out_completed and not _process.exit_code:544            for value in stdout:545                if 'instance' in value[1] and value[1] != '':546                    cloud_instance = json.loads(value[1])547                    cloud_server_id = _process.server_id548 549                if type(cloud_instance) is dict and \550                        'instance' in cloud_instance:551                    cloud_instance['instance']['sid'] = cloud_server_id552                    cloud_instance['instance']['status'] = True553                    cloud_instance['instance']['pid'] = _pid554                    return update_server(cloud_instance)555        elif err_completed and _process.exit_code is not None and \556                _process.exit_code > 0:557            cloud_instance = {'instance': {}}558            cloud_instance['instance']['sid'] = _process.server_id559            cloud_instance['instance']['status'] = False560            cloud_instance['instance']['pid'] = _pid561            return update_server(cloud_instance)562        else:563            clear_cloud_session(_pid)564        return True, {}565 566    def status(self, out=0, err=0):567        ctime = get_current_time(format='%y%m%d%H%M%S%f')568 569        stdout = []570        stderr = []571        out_completed = err_completed = False572        process_output = (out != -1 and err != -1)573 574        j = Process.query.filter_by(575            pid=self.id, user_id=current_user.id576        ).first()577        enc = sys.getdefaultencoding()578        if enc == 'ascii':579            enc = 'utf-8'580 581        execution_time = None582 583        if j is not None:584            _, updated = BatchProcess.update_process_info(j)585            if updated:586                db.session.commit()587            self.stime = j.start_time588            self.etime = j.end_time589            self.ecode = j.exit_code590 591            if self.stime is not None:592                stime = parser.parse(self.stime)593                etime = parser.parse(self.etime or get_current_time())594 595                execution_time = BatchProcess.total_seconds(etime - stime)596 597            if process_output:598                out, out_completed = self.read_log(599                    self.stdout, stdout, out, ctime, self.ecode, enc600                )601                err, err_completed = self.read_log(602                    self.stderr, stderr, err, ctime, self.ecode, enc603                )604        else:605            out_completed = err_completed = False606 607        if out == -1 or err == -1:608            return {609                'start_time': self.stime,610                'exit_code': self.ecode,611                'execution_time': execution_time,612                'process_state': self.process_state613            }614 615        return {616            'out': {617                'pos': out,618                'lines': stdout,619                'done': out_completed620            },621            'err': {622                'pos': err,623                'lines': stderr,624                'done': err_completed625            },626            'start_time': self.stime,627            'exit_code': self.ecode,628            'execution_time': execution_time629        }630 631    @staticmethod632    def _check_start_time(p, data):633        """634        Check start time and its related other timing checks.635        :param p: Process.636        :param data: Data637        :return:638        """639        if 'start_time' in data and data['start_time']:640            p.start_time = data['start_time']641 642            # We can't have 'exit_code' without the 'start_time'643            if 'exit_code' in data and \644                    data['exit_code'] is not None:645                p.exit_code = data['exit_code']646 647                # We can't have 'end_time' without the 'exit_code'.648                if 'end_time' in data and data['end_time']:649                    p.end_time = data['end_time']650 651    @staticmethod652    def update_process_info(p):653        if p.start_time is None or p.end_time is None:654            status = os.path.join(p.logdir, 'status')655            if not os.path.isfile(status):656                return False, False657 658            with open(status, 'r') as fp:659                import json660                try:661                    data = json.load(fp)662 663                    #  First - check for the existance of 'start_time'.664                    BatchProcess._check_start_time(p, data)665                    # get the pid of the utility.666                    if 'pid' in data:667                        p.utility_pid = data['pid']668 669                    return True, True670 671                except ValueError as e:672                    current_app.logger.warning(673                        _("Status for the background process '{0}' could "674                          "not be loaded.").format(p.pid)675                    )676                    current_app.logger.exception(e)677                    return False, False678        return True, False679 680    @staticmethod681    def _check_process_desc(p):682        """683        Check process desc instance and return data according to process.684        :param p: process685        :return: return value for details, type_desc and desc related686        to process687        """688        try:689            desc = loads(bytes.fromhex(p.desc))690        except Exception:691            desc = loads(p.desc)692 693        details = desc694        type_desc = ''695        current_storage_dir = None696 697        if isinstance(desc, IProcessDesc):698 699            from pgadmin.tools.backup import BackupMessage700            from pgadmin.tools.import_export import IEMessage701            args = []702            args_csv = StringIO(703                p.arguments.encode('utf-8')704                if hasattr(p.arguments, 'decode') else p.arguments705            )706            args_reader = csv.reader(args_csv, delimiter=str(','))707            for arg in args_reader:708                args = args + arg709            details = desc.details(p.command, args)710            type_desc = desc.type_desc711            if isinstance(desc, (BackupMessage, IEMessage)):712                current_storage_dir = desc.current_storage_dir713            desc = desc.message714 715        return desc, details, type_desc, current_storage_dir716 717    @staticmethod718    def list():719        processes = Process.query.filter_by(user_id=current_user.id)720        changed = False721 722        browser_preference = Preferences.module('browser')723        expiry_add = timedelta(724            browser_preference.preference('process_retain_days').get() or 1725        )726 727        res = []728        for p in [*processes]:729            if p.start_time is not None:730                # remove expired jobs731                process_expiration_time = \732                    parser.parse(p.start_time) + expiry_add733                if datetime.now(process_expiration_time.tzinfo) >= \734                        process_expiration_time:735                    shutil.rmtree(p.logdir, True)736                    db.session.delete(p)737                    changed = True738 739            status, updated = BatchProcess.update_process_info(p)740            if not status:741                continue742            elif not changed:743                changed = updated744 745            if p.start_time is None or (746                p.acknowledge is not None and p.end_time is None747            ):748                continue749 750            stime = parser.parse(p.start_time)751            etime = parser.parse(p.end_time or get_current_time())752 753            execution_time = BatchProcess.total_seconds(etime - stime)754 755            desc, details, type_desc, current_storage_dir = BatchProcess.\756                _check_process_desc(p)757 758            res.append({759                'id': p.pid,760                'desc': desc,761                'type_desc': type_desc,762                'details': details,763                'stime': stime,764                'etime': p.end_time,765                'exit_code': p.exit_code,766                'acknowledge': p.acknowledge,767                'execution_time': execution_time,768                'process_state': p.process_state,769                'utility_pid': p.utility_pid,770                'server_id': p.server_id,771                'current_storage_dir': current_storage_dir,772            })773 774        if changed:775            db.session.commit()776 777        return res778 779    @staticmethod780    def total_seconds(dt):781        return round(dt.total_seconds(), 2)782 783    @staticmethod784    def acknowledge(_pid):785        """786        Acknowledge from the user, he/she has alredy watched the status.787 788        Update the acknowledgement status, if the process is still running.789        And, delete the process information from the configuration, and the log790        files related to the process, if it has already been completed.791        """792        p = Process.query.filter_by(793            user_id=current_user.id, pid=_pid794        ).first()795 796        if p is None:797            raise LookupError(PROCESS_NOT_FOUND)798 799        if p.end_time is not None:800            logdir = p.logdir801            db.session.delete(p)802            import shutil803            shutil.rmtree(logdir, True)804        else:805            p.acknowledge = get_current_time()806 807        db.session.commit()808 809    def set_env_variables(self, server, **kwargs):810        """Set environment variables"""811        if server:812            # Set SSL related ENV variables813            if hasattr(server, 'connection_params') and \814                server.connection_params and \815                'sslcert' in server.connection_params and \816                'sslkey' in server.connection_params and \817                    'sslrootcert' in server.connection_params:818                # SSL environment variables819                sslcert = get_complete_file_path(820                    server.connection_params['sslcert'])821                sslkey = get_complete_file_path(822                    server.connection_params['sslkey'])823                sslrootcert = get_complete_file_path(824                    server.connection_params['sslrootcert'])825 826                self.env['PGSSLMODE'] = server.connection_params['sslmode'] \827                    if hasattr(server, 'connection_params') and \828                    'sslmode' in server.connection_params else 'prefer'829                self.env['PGSSLCERT'] = '' if sslcert is None else sslcert830                self.env['PGSSLKEY'] = '' if sslkey is None else sslkey831                self.env['PGSSLROOTCERT'] = \832                    '' if sslrootcert is None else sslrootcert833 834            # Set service name related ENV variable835            if server.service:836                self.env['PGSERVICE'] = server.service837 838        if self.manager_obj:839            # Set the PGPASSFILE environment variable840            if self.manager_obj.connection_params and \841                isinstance(self.manager_obj.connection_params, dict) and \842                'passfile' in self.manager_obj.connection_params and \843                    self.manager_obj.connection_params['passfile']:844                pgpasspath = get_complete_file_path(845                    self.manager_obj.connection_params['passfile'])846                if pgpasspath is not None:847                    self.env['PGPASSFILE'] = pgpasspath848 849            # Check for connection timeout and if it is greater than 0 then850            # set the environment variable PGCONNECT_TIMEOUT.851            timeout = self.manager_obj.get_connection_param_value(852                'connect_timeout')853            if timeout and int(timeout) > 0:854                self.env['PGCONNECT_TIMEOUT'] = str(timeout)855 856            # export password environment857            self.manager_obj.export_password_env(self.id)858 859        if 'env' in kwargs:860            self.env.update(kwargs['env'])861 862    @staticmethod863    def stop_process(_pid):864        """865        """866        p = Process.query.filter_by(867            user_id=current_user.id, pid=_pid868        ).first()869 870        if p is None:871            raise LookupError(PROCESS_NOT_FOUND)872 873        try:874            process = psutil.Process(p.utility_pid)875            process.terminate()876            # Update the process state to "Terminated"877            p.process_state = PROCESS_TERMINATED878        except psutil.NoSuchProcess:879            p.process_state = PROCESS_TERMINATED880        except psutil.Error as e:881            current_app.logger.warning(882                _("Unable to kill the background process '{0}'").format(883                    p.utility_pid)884            )885            current_app.logger.exception(e)886        db.session.commit()887 888    @staticmethod889    def update_server_id(_pid, _sid):890        p = Process.query.filter_by(891            user_id=current_user.id, pid=_pid892        ).first()893 894        if p is None:895            raise LookupError(PROCESS_NOT_FOUND)896 897        # Update the cloud server id898        p.server_id = _sid899        db.session.commit()900 
codekingpro/portable-devtools · Team Ai