mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-27 11:10:19 -06:00
388 lines
11 KiB
Python
388 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
##########################################################################
|
|
#
|
|
# pgAdmin 4 - PostgreSQL Tools
|
|
#
|
|
# Copyright (C) 2013 - 2016, The pgAdmin Development Team
|
|
# This software is released under the PostgreSQL License
|
|
#
|
|
##########################################################################
|
|
|
|
"""
|
|
Introduce a function to run the process executor in detached mode.
|
|
"""
|
|
from __future__ import print_function, unicode_literals
|
|
|
|
import csv
|
|
import os
|
|
import sys
|
|
from abc import ABCMeta, abstractproperty, abstractmethod
|
|
from datetime import datetime
|
|
from pickle import dumps, loads
|
|
from subprocess import Popen, PIPE
|
|
|
|
import pytz
|
|
from dateutil import parser
|
|
from flask import current_app as app
|
|
from flask_babel import gettext as _
|
|
from flask_security import current_user
|
|
|
|
import config
|
|
from pgadmin.model import Process, db
|
|
|
|
if sys.version_info < (3,):
|
|
from StringIO import StringIO
|
|
else:
|
|
from io import StringIO
|
|
|
|
|
|
def get_current_time(format='%Y-%m-%d %H:%M:%S.%f %z'):
|
|
"""
|
|
Generate the current time string in the given format.
|
|
"""
|
|
return datetime.utcnow().replace(
|
|
tzinfo=pytz.utc
|
|
).strftime(format)
|
|
|
|
|
|
class IProcessDesc(object):
|
|
__metaclass__ = ABCMeta
|
|
|
|
@abstractproperty
|
|
def message(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def details(self, cmd, args):
|
|
pass
|
|
|
|
|
|
class BatchProcess(object):
|
|
def __init__(self, **kwargs):
|
|
|
|
self.id = self.desc = self.cmd = self.args = self.log_dir = \
|
|
self.stdout = self.stderr = self.stime = self.etime = \
|
|
self.ecode = None
|
|
|
|
if 'id' in kwargs:
|
|
self._retrieve_process(kwargs['id'])
|
|
else:
|
|
self._create_process(kwargs['desc'], kwargs['cmd'], kwargs['args'])
|
|
|
|
def _retrieve_process(self, _id):
|
|
p = Process.query.filter_by(pid=_id, user_id=current_user.id).first()
|
|
|
|
if p is None:
|
|
raise LookupError(
|
|
_("Could not find a process with the specified ID.")
|
|
)
|
|
|
|
# ID
|
|
self.id = _id
|
|
# Description
|
|
self.desc = loads(p.desc)
|
|
# Status Acknowledged time
|
|
self.atime = p.acknowledge
|
|
# Command
|
|
self.cmd = p.command
|
|
# Arguments
|
|
self.args = p.arguments
|
|
# Log Directory
|
|
self.log_dir = p.logdir
|
|
# Standard ouput log file
|
|
self.stdout = os.path.join(p.logdir, 'out')
|
|
# Standard error log file
|
|
self.stderr = os.path.join(p.logdir, 'err')
|
|
# Start time
|
|
self.stime = p.start_time
|
|
# End time
|
|
self.etime = p.end_time
|
|
# Exit code
|
|
self.ecode = p.exit_code
|
|
|
|
def _create_process(self, _desc, _cmd, _args):
|
|
ctime = get_current_time(format='%y%m%d%H%M%S%f')
|
|
log_dir = os.path.join(
|
|
config.SESSION_DB_PATH, 'process_logs'
|
|
)
|
|
|
|
def random_number(size):
|
|
import random
|
|
import string
|
|
|
|
return ''.join(
|
|
random.choice(
|
|
string.ascii_uppercase + string.digits
|
|
) for _ in range(size)
|
|
)
|
|
|
|
created = False
|
|
size = 0
|
|
id = ctime
|
|
while not created:
|
|
try:
|
|
id += random_number(size)
|
|
log_dir = os.path.join(log_dir, id)
|
|
size += 1
|
|
if not os.path.exists(log_dir):
|
|
os.makedirs(log_dir, int('700', 8))
|
|
created = True
|
|
except OSError as oe:
|
|
import errno
|
|
if oe.errno != errno.EEXIST:
|
|
raise
|
|
|
|
# ID
|
|
self.id = ctime
|
|
# Description
|
|
self.desc = _desc
|
|
# Status Acknowledged time
|
|
self.atime = None
|
|
# Command
|
|
self.cmd = _cmd
|
|
# Log Directory
|
|
self.log_dir = log_dir
|
|
# Standard ouput log file
|
|
self.stdout = os.path.join(log_dir, 'out')
|
|
# Standard error log file
|
|
self.stderr = os.path.join(log_dir, 'err')
|
|
# Start time
|
|
self.stime = None
|
|
# End time
|
|
self.etime = None
|
|
# Exit code
|
|
self.ecode = None
|
|
|
|
# Arguments
|
|
args_csv_io = StringIO()
|
|
csv_writer = csv.writer(
|
|
args_csv_io, delimiter=str(','), quoting=csv.QUOTE_MINIMAL
|
|
)
|
|
csv_writer.writerow(_args)
|
|
self.args = args_csv_io.getvalue().strip(str('\r\n'))
|
|
|
|
j = Process(
|
|
pid=int(id), command=_cmd, arguments=self.args, logdir=log_dir,
|
|
desc=dumps(self.desc), user_id=current_user.id
|
|
)
|
|
db.session.add(j)
|
|
db.session.commit()
|
|
|
|
def start(self):
|
|
if self.stime is not None:
|
|
if self.etime is None:
|
|
raise Exception(_('The process has already been started.'))
|
|
raise Exception(
|
|
_('The process has already finished and can not be restarted.')
|
|
)
|
|
|
|
executor = os.path.join(
|
|
os.path.dirname(__file__), 'process_executor.py'
|
|
)
|
|
|
|
p = None
|
|
cmd = [
|
|
(sys.executable if not app.PGADMIN_RUNTIME else
|
|
'pythonw.exe' if os.name == 'nt' else 'python'),
|
|
executor,
|
|
'-p', self.id,
|
|
'-o', self.log_dir,
|
|
'-d', config.SQLITE_PATH
|
|
]
|
|
|
|
if os.name == 'nt':
|
|
p = Popen(
|
|
cmd, stdout=None, stderr=None, stdin=None, close_fds=True,
|
|
shell=False, creationflags=0x00000008
|
|
)
|
|
else:
|
|
def preexec_function():
|
|
import signal
|
|
# Detaching from the parent process group
|
|
os.setpgrp()
|
|
# Explicitly ignoring signals in the child process
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
|
|
p = Popen(
|
|
cmd, stdout=PIPE, stderr=None, stdin=None, close_fds=True,
|
|
shell=False, preexec_fn=preexec_function
|
|
)
|
|
|
|
self.ecode = p.poll()
|
|
if self.ecode is not None:
|
|
# TODO:: Couldn't start execution
|
|
pass
|
|
|
|
def status(self, out=0, err=0):
|
|
import codecs
|
|
ctime = get_current_time(format='%Y%m%d%H%M%S%f')
|
|
|
|
stdout = []
|
|
stderr = []
|
|
out_completed = err_completed = False
|
|
process_output = (out != -1 and err != -1)
|
|
|
|
def read_log(logfile, log, pos, ctime, check=True):
|
|
completed = True
|
|
lines = 0
|
|
|
|
if not os.path.isfile(logfile):
|
|
return 0, False
|
|
|
|
with codecs.open(logfile, 'r', 'utf-8') as stream:
|
|
stream.seek(pos)
|
|
for line in stream:
|
|
logtime = StringIO()
|
|
idx = 0
|
|
for c in line:
|
|
idx += 1
|
|
if c == ',':
|
|
break
|
|
logtime.write(c)
|
|
logtime = logtime.getvalue()
|
|
|
|
if check and logtime > ctime:
|
|
completed = False
|
|
break
|
|
if lines == 5120:
|
|
ctime = logtime
|
|
completed = False
|
|
break
|
|
|
|
lines += 1
|
|
log.append([logtime, line[idx:]])
|
|
pos = stream.tell()
|
|
|
|
return pos, completed
|
|
|
|
if process_output:
|
|
out, out_completed = read_log(
|
|
self.stdout, stdout, out, ctime, True
|
|
)
|
|
err, err_completed = read_log(
|
|
self.stderr, stderr, err, ctime, True
|
|
)
|
|
|
|
j = Process.query.filter_by(
|
|
pid=self.id, user_id=current_user.id
|
|
).first()
|
|
|
|
execution_time = None
|
|
|
|
if j is not None:
|
|
self.stime = j.start_time
|
|
self.etime = j.end_time
|
|
self.ecode = j.exit_code
|
|
|
|
if self.stime is not None:
|
|
stime = parser.parse(self.stime)
|
|
etime = parser.parse(self.etime or get_current_time())
|
|
|
|
execution_time = (etime - stime).total_seconds()
|
|
|
|
if process_output and self.ecode is not None and (
|
|
len(stdout) + len(stderr) < 3073
|
|
):
|
|
out, out_completed = read_log(
|
|
self.stdout, stdout, out, ctime, False
|
|
)
|
|
err, err_completed = read_log(
|
|
self.stderr, stderr, err, ctime, False
|
|
)
|
|
else:
|
|
out_completed = err_completed = False
|
|
|
|
if out == -1 or err == -1:
|
|
return {
|
|
'exit_code': self.ecode,
|
|
'execution_time': execution_time
|
|
}
|
|
|
|
return {
|
|
'out': {'pos': out, 'lines': stdout, 'done': out_completed},
|
|
'err': {'pos': err, 'lines': stderr, 'done': err_completed},
|
|
'exit_code': self.ecode,
|
|
'execution_time': execution_time
|
|
}
|
|
|
|
@staticmethod
|
|
def list():
|
|
processes = Process.query.filter_by(user_id=current_user.id)
|
|
|
|
res = []
|
|
for p in processes:
|
|
if p.start_time is None or p.acknowledge is not None:
|
|
continue
|
|
execution_time = None
|
|
|
|
stime = parser.parse(p.start_time)
|
|
etime = parser.parse(p.end_time or get_current_time())
|
|
|
|
execution_time = (etime - stime).total_seconds()
|
|
desc = loads(p.desc)
|
|
details = desc
|
|
|
|
if isinstance(desc, IProcessDesc):
|
|
args = []
|
|
args_csv = StringIO(p.arguments)
|
|
args_reader = csv.reader(args_csv, delimiter=str(','))
|
|
for arg in args_reader:
|
|
args = args + arg
|
|
details = desc.details(p.command, args)
|
|
desc = desc.message
|
|
|
|
res.append({
|
|
'id': p.pid,
|
|
'desc': desc,
|
|
'details': details,
|
|
'stime': stime,
|
|
'etime': p.end_time,
|
|
'exit_code': p.exit_code,
|
|
'acknowledge': p.acknowledge,
|
|
'execution_time': execution_time
|
|
})
|
|
|
|
return res
|
|
|
|
@staticmethod
|
|
def acknowledge(_pid, _release):
|
|
p = Process.query.filter_by(
|
|
user_id=current_user.id, pid=_pid
|
|
).first()
|
|
|
|
if p is None:
|
|
raise LookupError(
|
|
_("Could not find a process with the specified ID.")
|
|
)
|
|
|
|
if _release:
|
|
import shutil
|
|
shutil.rmtree(p.logdir, True)
|
|
db.session.delete(p)
|
|
else:
|
|
p.acknowledge = get_current_time()
|
|
|
|
db.session.commit()
|
|
|
|
@staticmethod
|
|
def release(pid=None):
|
|
import shutil
|
|
processes = None
|
|
|
|
if pid is not None:
|
|
processes = Process.query.filter_by(
|
|
user_id=current_user.id, pid=pid
|
|
)
|
|
else:
|
|
processes = Process.query.filter_by(
|
|
user_id=current_user.id,
|
|
acknowledge=None
|
|
)
|
|
|
|
if processes:
|
|
for p in processes:
|
|
shutil.rmtree(p.logdir, True)
|
|
|
|
db.session.delete(p)
|
|
db.session.commit()
|