Added PSQL tool support for the Windows platform.

Fixed following issues:
1. If the database name contains escape characters then PSQL unable to connect.
2. If the user terminates the connection by typing the 'exit' command, PSQL
   will show the connection termination msg.

Fixes #2341
This commit is contained in:
Nikhil Mohite 2021-06-08 14:58:43 +05:30 committed by Akshay Joshi
parent 5a086a9173
commit 9f12747d9b
10 changed files with 220 additions and 163 deletions

View File

@ -37,6 +37,7 @@ ldap3==2.*
Flask-BabelEx==0.*
gssapi==1.6.*
flask-socketio>=5.0.1
eventlet==0.30.2
eventlet==0.31.0
httpagentparser==1.9.*
user-agents==2.2.0
pywinpty==1.1.1; sys_platform=="win32"

View File

@ -506,61 +506,38 @@ def register_browser_preferences(self):
)
)
if sys.platform != 'win32':
self.open_in_new_tab = self.preference.register(
'tab_settings', 'new_browser_tab_open',
gettext("Open in new browser tab"), 'select2', None,
category_label=PREF_LABEL_OPTIONS,
options=[{'label': gettext('Query Tool'), 'value': 'qt'},
{'label': gettext('Debugger'), 'value': 'debugger'},
{'label': gettext('Schema Diff'), 'value': 'schema_diff'},
{'label': gettext('ERD Tool'), 'value': 'erd_tool'},
{'label': gettext('PSQL Tool'), 'value': 'psql_tool'}],
help_str=gettext(
'Select Query Tool, Debugger, Schema Diff, ERD Tool '
'or PSQL Tool from the drop-down to set '
'open in new browser tab for that particular module.'
),
select2={
'multiple': True, 'allowClear': False,
'tags': True, 'first_empty': False,
'selectOnClose': False, 'emptyOptions': True,
'tokenSeparators': [','],
'placeholder': gettext('Select open new tab...')
}
)
self.open_in_new_tab = self.preference.register(
'tab_settings', 'new_browser_tab_open',
gettext("Open in new browser tab"), 'select2', None,
category_label=PREF_LABEL_OPTIONS,
options=[{'label': gettext('Query Tool'), 'value': 'qt'},
{'label': gettext('Debugger'), 'value': 'debugger'},
{'label': gettext('Schema Diff'), 'value': 'schema_diff'},
{'label': gettext('ERD Tool'), 'value': 'erd_tool'},
{'label': gettext('PSQL Tool'), 'value': 'psql_tool'}],
help_str=gettext(
'Select Query Tool, Debugger, Schema Diff, ERD Tool '
'or PSQL Tool from the drop-down to set '
'open in new browser tab for that particular module.'
),
select2={
'multiple': True, 'allowClear': False,
'tags': True, 'first_empty': False,
'selectOnClose': False, 'emptyOptions': True,
'tokenSeparators': [','],
'placeholder': gettext('Select open new tab...')
}
)
self.psql_tab_title = self.preference.register(
'tab_settings', 'psql_tab_title_placeholder',
gettext("PSQL tool tab title"),
'text', '%DATABASE%/%USERNAME%@%SERVER%',
category_label=PREF_LABEL_DISPLAY,
help_str=gettext(
'Supported placeholders are %DATABASE%, %USERNAME%, '
'and %SERVER%. Users can provide any string with or without'
' placeholders of their choice. The blank title will be revert'
' back to the default title with placeholders.'
)
)
else:
self.open_in_new_tab = self.preference.register(
'tab_settings', 'new_browser_tab_open',
gettext("Open in new browser tab"), 'select2', None,
category_label=PREF_LABEL_OPTIONS,
options=[{'label': gettext('Query Tool'), 'value': 'qt'},
{'label': gettext('Debugger'), 'value': 'debugger'},
{'label': gettext('Schema Diff'), 'value': 'schema_diff'},
{'label': gettext('ERD Tool'), 'value': 'erd_tool'}],
help_str=gettext(
'Select Query Tool, Debugger, Schema Diff, ERD Tool '
'or PSQL Tool from the drop-down to set '
'open in new browser tab for that particular module.'
),
select2={
'multiple': True, 'allowClear': False,
'tags': True, 'first_empty': False,
'selectOnClose': False, 'emptyOptions': True,
'tokenSeparators': [','],
'placeholder': gettext('Select open new tab...')
}
self.psql_tab_title = self.preference.register(
'tab_settings', 'psql_tab_title_placeholder',
gettext("PSQL tool tab title"),
'text', '%DATABASE%/%USERNAME%@%SERVER%',
category_label=PREF_LABEL_DISPLAY,
help_str=gettext(
'Supported placeholders are %DATABASE%, %USERNAME%, '
'and %SERVER%. Users can provide any string with or without'
' placeholders of their choice. The blank title will be revert'
' back to the default title with placeholders.'
)
)

View File

@ -66,7 +66,7 @@ define([
}]);
// show psql tool same as query tool.
if(pgAdmin['enable_psql'] && pgAdmin['platform'] != 'win32') {
if(pgAdmin['enable_psql']) {
pgAdmin.Browser.add_menus([{
name: 'show_psql_tool', node: this.type, module: this,
applies: ['context'], callback: 'show_psql_tool',

View File

@ -210,7 +210,7 @@ define('pgadmin.browser.node', [
icon: 'fa fa-search', enable: enable,
}]);
if(pgAdmin['enable_psql'] && pgAdmin['platform'] != 'win32') {
if(pgAdmin['enable_psql']) {
// show psql tool same as query tool.
pgAdmin.Browser.add_menus([{
name: 'show_psql_tool', node: this.type, module: this,

View File

@ -59,7 +59,7 @@ let _defaultToolBarButtons = [
}
];
if(pgAdmin['enable_psql'] && pgAdmin['platform'] != 'win32') {
if(pgAdmin['enable_psql']) {
_defaultToolBarButtons.unshift({
label: gettext('PSQL Tool'),
ariaLabel: gettext('PSQL Tool'),
@ -119,7 +119,7 @@ export function initializeToolbar(panel, wcDocker) {
pgAdmin.DataGrid.show_filtered_row({mnuid: 4}, pgAdmin.Browser.tree.selected());
else if ('name' in data && data.name === gettext('Search objects'))
pgAdmin.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected());
else if ('name' in data && data.name === gettext('PSQL Tool') && pgAdmin['platform'] != 'win32'){
else if ('name' in data && data.name === gettext('PSQL Tool')){
var input = {},
t = pgAdmin.Browser.tree,
i = input.item || t.selected(),

View File

@ -63,7 +63,8 @@ def underscore_unescape(text):
""": '"',
"`": '`',
"'": "'",
"'": "'"
"'": "'",
""": '"'
}
# always replace & first

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
import os
import re
import select
@ -20,7 +19,9 @@ from pgadmin.utils.driver import get_driver
from ... import socketio as sio
from pgadmin.utils import get_complete_file_path
if _platform != 'win32':
if _platform == 'win32':
from winpty import PtyProcess
else:
import fcntl
import termios
import pty
@ -101,8 +102,8 @@ def panel(trans_id):
return render_template('editor_template.html',
sid=params['sid'],
db=underscore_unescape(params['db']) if params[
'db'] else 'postgres',
db=underscore_unescape(
o_db_name) if o_db_name else 'postgres',
server_type=params['server_type'],
is_enable=config.ENABLE_PSQL,
title=underscore_unescape(params['title']),
@ -120,8 +121,13 @@ def set_term_size(fd, row, col, xpix=0, ypix=0):
:param xpix:
:param ypix:
"""
term_size = struct.pack('HHHH', row, col, xpix, ypix)
fcntl.ioctl(fd, termios.TIOCSWINSZ, term_size)
if _platform == 'win32':
app.config['sessions'][request.sid].setwinsize(row, col)
# data = {'key_name': 'Enter', 'input': '\n'}
# socket_input(data)
else:
term_size = struct.pack('HHHH', row, col, xpix, ypix)
fcntl.ioctl(fd, termios.TIOCSWINSZ, term_size)
@sio.on('connect', namespace='/pty')
@ -216,6 +222,19 @@ def read_terminal_data(parent, data_ready, max_read_bytes, sid):
session_last_cmd[request.sid]['invalid_cmd'] = False
def read_stdout(process, sid, max_read_bytes, win_emit_output=True):
(data_ready, _, _) = select.select([process.fd], [], [], 0)
if process.fd in data_ready:
output = process.read(max_read_bytes)
if win_emit_output:
sio.emit('pty-output',
{'result': output,
'error': False},
namespace='/pty', room=sid)
sio.sleep(0)
@sio.on('start_process', namespace='/pty')
def start_process(data):
"""
@ -227,8 +246,24 @@ def start_process(data):
def read_and_forward_pty_output(sid, data):
max_read_bytes = 1024 * 20
import time
if _platform == 'win32':
os.environ['PYWINPTY_BACKEND'] = '1'
process = PtyProcess.spawn('cmd.exe')
process.write(r'"{0}" "{1}" 2>>&1'.format(connection_data[0],
connection_data[1]))
process.write("\r\n")
app.config['sessions'][request.sid] = process
pdata[request.sid] = process
set_term_size(process, 50, 50)
while True:
read_stdout(process, sid, max_read_bytes,
win_emit_output=True)
else:
if _platform != 'win32':
p, parent, fd = create_pty_terminal(connection_data)
while p and p.poll() is None:
@ -248,12 +283,6 @@ def start_process(data):
timeout)
read_terminal_data(parent, data_ready, max_read_bytes, sid)
else:
sio.emit(
'conn_error',
{
'error': 'PSQL tool not supported.',
}, namespace='/pty', room=request.sid)
# Check user is authenticated and PSQL is enabled in config.
if current_user.is_authenticated and config.ENABLE_PSQL:
@ -342,14 +371,14 @@ def get_connection_str(psql_utility, db, manager):
:param db: database name to connect specific db.
:return: connection attribute list for PSQL connection.
"""
conn_attr = get_conn_str(manager, db)
conn_attr = get_conn_str_win(manager, db)
conn_attr_list = list()
conn_attr_list.append(psql_utility)
conn_attr_list.append(conn_attr)
return conn_attr_list
def get_conn_str(manager, db):
def get_conn_str_win(manager, db):
"""
Get connection attributes for psql connection.
:param manager:
@ -357,46 +386,48 @@ def get_conn_str(manager, db):
:return:
"""
manager.export_password_env('PGPASSWORD')
db = db.replace('"', '\\"')
db = db.replace("'", "\\'")
conn_attr =\
'host={0} port={1} dbname={2} user={3} sslmode={4} ' \
'sslcompression={5} ' \
'host=\'{0}\' port=\'{1}\' dbname=\'{2}\' user=\'{3}\' ' \
'sslmode=\'{4}\' sslcompression=\'{5}\' ' \
''.format(
manager.local_bind_host if manager.use_ssh_tunnel else
manager.host,
manager.local_bind_port if manager.use_ssh_tunnel else
manager.port,
underscore_unescape(db) if db != '' else 'postgres',
db if db != '' else 'postgres',
underscore_unescape(manager.user) if manager.user else 'postgres',
manager.ssl_mode,
True if manager.sslcompression else False,
)
if manager.hostaddr:
conn_attr = " {0} hostaddr={1}".format(conn_attr, manager.hostaddr)
conn_attr = " {0} hostaddr='{1}'".format(conn_attr, manager.hostaddr)
if manager.passfile:
conn_attr = " {0} passfile={1}".format(conn_attr,
get_complete_file_path(
manager.passfile))
conn_attr = " {0} passfile='{1}'".format(conn_attr,
get_complete_file_path(
manager.passfile))
if get_complete_file_path(manager.sslcert):
conn_attr = " {0} sslcert={1}".format(
conn_attr = " {0} sslcert='{1}'".format(
conn_attr, get_complete_file_path(manager.sslcert))
if get_complete_file_path(manager.sslkey):
conn_attr = " {0} sslkey={1}".format(
conn_attr = " {0} sslkey='{1}'".format(
conn_attr, get_complete_file_path(manager.sslkey))
if get_complete_file_path(manager.sslrootcert):
conn_attr = " {0} sslrootcert={1}".format(
conn_attr = " {0} sslrootcert='{1}'".format(
conn_attr, get_complete_file_path(manager.sslrootcert))
if get_complete_file_path(manager.sslcrl):
conn_attr = " {0} sslcrl={1}".format(
conn_attr = " {0} sslcrl='{1}'".format(
conn_attr, get_complete_file_path(manager.sslcrl))
if manager.service:
conn_attr = " {0} service={1}".format(
conn_attr = " {0} service='{1}'".format(
conn_attr, get_complete_file_path(manager.service))
return conn_attr
@ -433,12 +464,27 @@ def invalid_cmd():
:rtype:
"""
session_last_cmd[request.sid]['invalid_cmd'] = True
for i in range(len(session_input[request.sid])):
os.write(app.config['sessions'][request.sid],
'\b \b'.encode())
if _platform == 'win32':
for i in range(len(session_input[request.sid])):
app.config['sessions'][request.sid].write('\b \b')
app.config['sessions'][request.sid].write('\r\n')
os.write(app.config['sessions'][request.sid],
'\n'.encode())
sio.emit(
'pty-output',
{
'result': gettext(
"ERROR: Shell commands are disabled "
"in psql for security\r\n"),
'error': True
},
namespace='/pty', room=request.sid)
else:
for i in range(len(session_input[request.sid])):
os.write(app.config['sessions'][request.sid],
'\b \b'.encode())
os.write(app.config['sessions'][request.sid],
'\n'.encode())
session_input[request.sid] = ''
@ -464,18 +510,36 @@ def check_valid_cmd(user_input):
if stop_execution:
session_last_cmd[request.sid]['invalid_cmd'] = True
if _platform == 'win32':
# Remove already added command from terminal.
for i in range(len(user_input)):
app.config['sessions'][request.sid].write('\b \b')
app.config['sessions'][request.sid].write('\n')
# Remove already added command from terminal.
for i in range(len(user_input)):
sio.emit(
'pty-output',
{
'result': gettext(
"ERROR: Shell commands are disabled "
"in psql for security\r\n"),
'error': True
},
namespace='/pty', room=request.sid)
else:
# Remove already added command from terminal.
for i in range(len(user_input)):
os.write(app.config['sessions'][request.sid],
'\b \b'.encode())
# Add Enter event to execute the command.
os.write(app.config['sessions'][request.sid],
'\b \b'.encode())
# Add Enter event to execute the command.
os.write(app.config['sessions'][request.sid],
'\n'.encode())
'\n'.encode())
else:
session_last_cmd[request.sid]['invalid_cmd'] = False
os.write(app.config['sessions'][request.sid],
'\n'.encode())
if _platform == 'win32':
app.config['sessions'][request.sid].write('\n')
else:
os.write(app.config['sessions'][request.sid],
'\n'.encode())
def enter_key_press(data):
@ -501,8 +565,8 @@ def enter_key_press(data):
not config.ALLOW_PSQL_SHELL_COMMANDS and\
not session_last_cmd[request.sid]['is_new_connection']:
check_valid_cmd(user_input)
elif user_input == '\q' or user_input == 'q\\q' or \
user_input in ['exit', 'exit;']:
elif user_input == '\q' or user_input == 'q\\q' or user_input in ['exit',
'exit;']:
# If user enter \q to terminate the PSQL, emit the msg to
# notify user connection is terminated.
sio.emit('pty-output',
@ -514,12 +578,20 @@ def enter_key_press(data):
'error': True},
namespace='/pty', room=request.sid)
os.write(app.config['sessions'][request.sid],
'\n'.encode())
if _platform == 'win32':
app.config['sessions'][request.sid].write('\n')
del app.config['sessions'][request.sid]
else:
os.write(app.config['sessions'][request.sid],
'\n'.encode())
else:
os.write(app.config['sessions'][request.sid],
data['input'].encode())
if _platform == 'win32':
app.config['sessions'][request.sid].write(
"{0}".format(data['input']))
else:
os.write(app.config['sessions'][request.sid],
data['input'].encode())
session_input[request.sid] = ''
session_last_cmd[request.sid]['is_new_connection'] = False
@ -619,9 +691,13 @@ def other_key_press(data):
session_input[request.sid] = data['input']
session_input_cursor[request.sid] += 1
# Write user input to terminal parent fd.
os.write(app.config['sessions'][request.sid],
data['input'].encode())
if _platform == 'win32':
app.config['sessions'][request.sid].write(
"{0}".format(data['input']))
else:
# Write user input to terminal parent fd.
os.write(app.config['sessions'][request.sid],
data['input'].encode())
@sio.on('socket_input', namespace='/pty')
@ -697,11 +773,17 @@ def server_disconnect(data):
def disconnect_socket():
os.write(app.config['sessions'][request.sid], '\q\n'.encode())
sio.sleep(1)
os.close(app.config['sessions'][request.sid])
os.close(cdata[request.sid])
del app.config['sessions'][request.sid]
if _platform == 'win32':
if request.sid in app.config['sessions']:
process = app.config['sessions'][request.sid]
process.terminate()
del app.config['sessions'][request.sid]
else:
os.write(app.config['sessions'][request.sid], '\q\n'.encode())
sio.sleep(1)
os.close(app.config['sessions'][request.sid])
os.close(cdata[request.sid])
del app.config['sessions'][request.sid]
def _get_database(sid, did):

View File

@ -55,7 +55,7 @@ export function initialize(gettext, url_for, $, _, pgAdmin, csrfToken, Browser)
}];
this.enable_psql_tool = pgAdmin['enable_psql'];
if(pgAdmin['enable_psql'] && pgAdmin['platform'] != 'win32') {
if(pgAdmin['enable_psql']) {
pgBrowser.add_menus(menus);
}
@ -156,11 +156,12 @@ export function initialize(gettext, url_for, $, _, pgAdmin, csrfToken, Browser)
var tab_title_placeholder = pgBrowser.get_preferences_for_module('browser').psql_tab_title_placeholder;
panelTitle = generateTitle(tab_title_placeholder, title_data);
const [panelUrl, panelCloseUrl] = this.getPanelUrls(transId, panelTitle, parentData, gen);
const [panelUrl, panelCloseUrl, db_label] = this.getPanelUrls(transId, panelTitle, parentData, gen);
let psqlToolForm = `
<form id="psqlToolForm" action="${panelUrl}" method="post">
<input id="title" name="title" hidden />
<input id='db' value='${db_label}' hidden />
<input name="close_url" value="${panelCloseUrl}" hidden />
</form>
<script>
@ -228,9 +229,11 @@ export function initialize(gettext, url_for, $, _, pgAdmin, csrfToken, Browser)
+`&did=${parentData.database._id}`
+`&server_type=${parentData.server.server_type}`
+ `&theme=${theme}`;
let db_label = '';
if(parentData.database && parentData.database._id) {
let db_label = parentData.database._label.replace('\\', '\\\\');
db_label = _.escape(parentData.database._label.replace('\\', '\\\\'));
db_label = db_label.replace('\'', '\'');
db_label = db_label.replace('"', '\"');
openUrl += `&db=${db_label}`;
} else {
openUrl += `&db=${''}`;
@ -239,7 +242,7 @@ export function initialize(gettext, url_for, $, _, pgAdmin, csrfToken, Browser)
let closeUrl = url_for('psql.close', {
trans_id: transId,
});
return [openUrl, closeUrl];
return [openUrl, closeUrl, db_label];
},
psql_terminal: function() {
// theme colors
@ -375,4 +378,3 @@ export function initialize(gettext, url_for, $, _, pgAdmin, csrfToken, Browser)
return pgBrowser.psql;
}

View File

@ -23,41 +23,36 @@
require(
['sources/generated/psql_tool'],
function(pgBrowser) {
if (self.pgAdmin['platform'] != 'win32') {
const term = self.pgAdmin.Browser.psql.psql_terminal();
<!--Addon for fitAddon, webLinkAddon, SearchAddon -->
const fitAddon = self.pgAdmin.Browser.psql.psql_Addon(term);
<!-- Update the theme for terminal as per pgAdmin 4 theme.-->
self.pgAdmin.Browser.psql.set_theme(term);
<!-- Open the terminal -->
term.open(document.getElementById('psql-terminal'));
<!-- Socket-->
const socket = self.pgAdmin.Browser.psql.psql_socket();
self.pgAdmin.Browser.psql.psql_socket_io(socket, '{{is_enable}}', '{{sid}}', '{{db}}', '{{server_type}}', fitAddon, term);
self.pgAdmin.Browser.psql.psql_terminal_io(term, socket);
self.pgAdmin.Browser.psql.check_db_name_change('{{db}}', '{{o_db_name}}');
const term = self.pgAdmin.Browser.psql.psql_terminal();
<!--Addon for fitAddon, webLinkAddon, SearchAddon -->
const fitAddon = self.pgAdmin.Browser.psql.psql_Addon(term);
<!-- Update the theme for terminal as per pgAdmin 4 theme.-->
self.pgAdmin.Browser.psql.set_theme(term);
<!-- Open the terminal -->
term.open(document.getElementById('psql-terminal'));
<!-- Socket-->
const socket = self.pgAdmin.Browser.psql.psql_socket();
self.pgAdmin.Browser.psql.psql_socket_io(socket, '{{is_enable}}', '{{sid}}', '{{db | replace("'", "\'")| replace('"', '\"') | replace('\\', '\\\\')}}', '{{server_type}}', fitAddon, term);
self.pgAdmin.Browser.psql.psql_terminal_io(term, socket);
self.pgAdmin.Browser.psql.check_db_name_change('{{db}}', '{{o_db_name}}');
<!-- Resize the terminal -->
function fitToscreen(){
fitAddon.fit()
socket.emit("resize", {"cols": term.cols, "rows": term.rows})
}
function debounce(func, wait_ms) {
let timeout
return function(...args) {
const context = this
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(context, args), wait_ms)
}
}
const wait_ms = 50;;
window.onresize = debounce(fitToscreen, wait_ms)
} else {
document.getElementById('psql-terminal').innterHTML = 'PSQL not supported'
<!-- Resize the terminal -->
function fitToscreen(){
fitAddon.fit()
socket.emit("resize", {"cols": term.cols, "rows": term.rows})
}
function debounce(func, wait_ms) {
let timeout
return function(...args) {
const context = this
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(context, args), wait_ms)
}
}
const wait_ms = 50;;
window.onresize = debounce(fitToscreen, wait_ms)
});
{% endblock %}

View File

@ -769,6 +769,10 @@ def configure_preferences(default_binary_path=None):
set_default_binary_path(
default_binary_path[server], bin_paths, server)
bin_paths_server_based = json.dumps(bin_paths['pg_bin_paths'])
if server == 'ppas':
bin_paths_server_based = json.dumps(bin_paths['as_bin_paths'])
pref_bin_path = paths_pref.preference('{0}_bin_dir'.format(server))
user_pref = cur.execute(
'SELECT pid, uid FROM user_preferences '
@ -779,15 +783,10 @@ def configure_preferences(default_binary_path=None):
if user_pref_data:
cur.execute(
'UPDATE user_preferences SET value = ? WHERE pid = ?',
(pref_bin_path.default, pref_bin_path.pid)
(bin_paths_server_based, pref_bin_path.pid)
)
else:
if server == 'ppas':
params = (pref_bin_path.pid, 1,
json.dumps(bin_paths['as_bin_paths']))
else:
params = (pref_bin_path.pid, 1,
json.dumps(bin_paths['pg_bin_paths']))
params = (pref_bin_path.pid, 1, bin_paths_server_based)
cur.execute(
insert_preferences_query, params
)