codekingpro/portable-devtools
114k
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 