public inbox for [email protected]
help / color / mirror / Atom feedFrom: Nikhil Mohite <[email protected]>
To: Dave Page <[email protected]>
Cc: pgadmin-hackers <[email protected]>
Subject: Re: [pgAdmin][RM-2341]: Add menu option for starting PSQL
Date: Mon, 10 May 2021 19:31:47 +0530
Message-ID: <CAOBg0AMVyLBma8wsbZ-VQWF4q3OAhqWeRLpmrxVcM_YOKH+MDQ@mail.gmail.com> (raw)
In-Reply-To: <CA+OCxow9+UDMrsBBvJKW2AeAYaN4cO3w7kBGNKtsWhSM6_D7gQ@mail.gmail.com>
References: <CAOBg0AO6Tjksb+EOA_-O13yRzdL_2d3y6G74m=GGQS+Ymjv=0g@mail.gmail.com>
<CA+OCxow9+UDMrsBBvJKW2AeAYaN4cO3w7kBGNKtsWhSM6_D7gQ@mail.gmail.com>
Hi Dave/ Team,
PFA updated patch, sorry for the inconvenience, while cleanup I removed the
unwanted libraries but forgot to remove the code related to them.
On Mon, May 10, 2021 at 7:10 PM Dave Page <[email protected]> wrote:
> Hi
>
> On Mon, May 10, 2021 at 1:45 PM Nikhil Mohite <
> [email protected]> wrote:
>
>> Hi Hackers,
>>
>> Please find the attached patch for RM-2341
>> <https://redmine.postgresql.org/issues/2341;: Add Menu option for
>> starting PSQL.
>> 1. Added new Option PSQL Tool in Tools menu.
>> 2. Added the same option for Server and Database nodes from the tree view.
>>
>
> Unfortunately there's a trailing comma in package.json that makes it
> invalid. If I fix that, then I get the error below, so I'm guessing the
> intention was to actually include another package there?
>
> ERROR in ./pgadmin/tools/psql/static/js/psql_module.js 23:50-82
> Module not found: Error: Can't resolve 'local-echo-controller' in
> '/Users/dpage/git/pgadmin4/web/pgadmin/tools/psql/static/js'
> resolve 'local-echo-controller' in
> '/Users/dpage/git/pgadmin4/web/pgadmin/tools/psql/static/js'
> Parsed request is a module
> using description file: /Users/dpage/git/pgadmin4/web/package.json
> (relative path: ./pgadmin/tools/psql/static/js)
> aliased with mapping 'local-echo-controller':
> '/Users/dpage/git/pgadmin4/web/node_modules/local-echo' to
> '/Users/dpage/git/pgadmin4/web/node_modules/local-echo'
> using description file: /Users/dpage/git/pgadmin4/web/package.json
> (relative path: ./pgadmin/tools/psql/static/js)
> Field 'browser' doesn't contain a valid alias configuration
> root path /Users/dpage/git/pgadmin4/web
> using description file:
> /Users/dpage/git/pgadmin4/web/package.json (relative path:
> ./Users/dpage/git/pgadmin4/web/node_modules/local-echo)
> no extension
> Field 'browser' doesn't contain a valid alias configuration
>
> /Users/dpage/git/pgadmin4/web/Users/dpage/git/pgadmin4/web/node_modules/local-echo
> doesn't exist
> .js
> Field 'browser' doesn't contain a valid alias configuration
>
> /Users/dpage/git/pgadmin4/web/Users/dpage/git/pgadmin4/web/node_modules/local-echo.js
> doesn't exist
> .jsx
> Field 'browser' doesn't contain a valid alias configuration
>
> /Users/dpage/git/pgadmin4/web/Users/dpage/git/pgadmin4/web/node_modules/local-echo.jsx
> doesn't exist
> as directory
>
> /Users/dpage/git/pgadmin4/web/Users/dpage/git/pgadmin4/web/node_modules/local-echo
> doesn't exist
> using description file: /Users/dpage/git/pgadmin4/web/package.json
> (relative path: ./node_modules/local-echo)
> no extension
> Field 'browser' doesn't contain a valid alias configuration
> /Users/dpage/git/pgadmin4/web/node_modules/local-echo doesn't
> exist
> .js
> Field 'browser' doesn't contain a valid alias configuration
> /Users/dpage/git/pgadmin4/web/node_modules/local-echo.js
> doesn't exist
> .jsx
> Field 'browser' doesn't contain a valid alias configuration
> /Users/dpage/git/pgadmin4/web/node_modules/local-echo.jsx
> doesn't exist
> as directory
> /Users/dpage/git/pgadmin4/web/node_modules/local-echo doesn't
> exist
> @ ./pgadmin/tools/psql/static/js/index.js 17:19-43
>
> 2021-05-10 14:38:37: webpack 5.21.2 compiled with 1 error in 60041 ms
>
> --
> Dave Page
> Blog: https://pgsnake.blogspot.com
> Twitter: @pgsnake
>
> EDB: https://www.enterprisedb.com
>
Regards,
Nikhil Mohite
Attachments:
[application/octet-stream] RM_2341_V2.patch (111.7K, 3-RM_2341_V2.patch)
download | inline diff:
diff --git a/docs/en_US/developer_tools.rst b/docs/en_US/developer_tools.rst
index 1ff442cc..bb67e33a 100644
--- a/docs/en_US/developer_tools.rst
+++ b/docs/en_US/developer_tools.rst
@@ -16,3 +16,4 @@ PL/SQL code.
editgrid
schema_diff
erd_tool
+ psql_tool
diff --git a/docs/en_US/images/psql_tool.png b/docs/en_US/images/psql_tool.png
new file mode 100644
index 0000000000000000000000000000000000000000..c5f88ba5a573aa60bf7e08b8a38526dc73e8fcd5
GIT binary patch
literal 23736
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
P=p;Q)S3j3^P6<r_JTn~)
literal 0
HcmV?d00001
diff --git a/docs/en_US/psql_tool.rst b/docs/en_US/psql_tool.rst
new file mode 100644
index 00000000..e0b3287e
--- /dev/null
+++ b/docs/en_US/psql_tool.rst
@@ -0,0 +1,18 @@
+.. _psql_tool:
+
+******************
+`PSQL Tool`:index:
+******************
+
+PSQL tool allows user to connect to PostgreSQL server using psql terminal.
+
+* Login to PostgreSQL server through terminal in pgAdmin.
+* execute all type of commands.
+
+.. image:: images/psql_tool.png
+ :alt: PSQL tool window
+ :align: center
+
+You can open multiple instance of the PSQL tool in individual tabs simultaneously.
+To close the PSQL tool, click the *X* in the upper-right hand corner of the tab bar.
+
diff --git a/requirements.txt b/requirements.txt
index edd7000b..fb8f25cd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -36,3 +36,6 @@ sshtunnel==0.*
ldap3==2.*
Flask-BabelEx==0.*
gssapi==1.6.*
+
+flask-socketio>=5.0.1
+eventlet==0.30.2
diff --git a/web/config.py b/web/config.py
index 2643ef19..002a9951 100644
--- a/web/config.py
+++ b/web/config.py
@@ -156,8 +156,8 @@ X_FRAME_OPTIONS = "SAMEORIGIN"
# such as JavaScript, CSS, or pretty much anything that the browser loads.
# see https://content-security-policy.com/#source_list for more info
# e.g. "default-src https: data: 'unsafe-inline' 'unsafe-eval';"
-CONTENT_SECURITY_POLICY = "default-src http: data: blob: 'unsafe-inline' " \
- "'unsafe-eval';"
+CONTENT_SECURITY_POLICY = "default-src ws: http: data: blob: 'unsafe-inline'" \
+ " 'unsafe-eval';"
# STRICT_TRANSPORT_SECURITY_ENABLED when set to True will set the
# Strict-Transport-Security header
@@ -636,6 +636,19 @@ KRB_AUTO_CREATE_USER = True
KERBEROS_CCACHE_DIR = os.path.join(DATA_DIR, 'krbccache')
+# PSQL tool settings
+# This will enable PSQL tool in pgAdmin. So user can execute the commands using
+# PSQL terminal in pgAdmin.
+ENABLE_PSQL = True
+
+# ALLOW_PSQL_SHELL_COMMAND = True will disable the execution of os level
+# commands using meta command \! from PSQL terminal.
+# As PSQL allow user to execute the os level commands from the PSQL terminal
+# user can execute any system level command as per the system login user
+# privileges. Default this setting is set to False but if it set to True
+# User will able to execute the system level commands through PSQL terminal in
+# pgAdmin.
+ALLOW_PSQL_SHELL_COMMANDS = False
##########################################################################
# Local config settings
diff --git a/web/package.json b/web/package.json
index cc1e15ab..fc2e8193 100644
--- a/web/package.json
+++ b/web/package.json
@@ -103,6 +103,7 @@
"json-bignumber": "^1.0.1",
"karma-coverage": "^2.0.3",
"leaflet": "^1.5.1",
+
"lodash": "4.*",
"ml-matrix": "^6.5.0",
"moment": "^2.24.0",
@@ -118,13 +119,18 @@
"shim-loader": "^1.0.1",
"slickgrid": "git+https://github.com/6pac/SlickGrid.git#2.3.16",
"snapsvg-cjs": "^0.0.6",
+ "socket.io-client": "^4.0.0",
"split.js": "^1.5.10",
"tablesorter": "^2.31.2",
"tempusdominus-bootstrap-4": "^5.1.2",
"tempusdominus-core": "^5.0.3",
"underscore": "^1.9.1",
"webcabin-docker": "git+https://github.com/EnterpriseDB/wcDocker/#c4a3398b89588408dc705895675bce7bd7660d36",
- "wkx": "^0.5.0"
+ "wkx": "^0.5.0",
+ "xterm": "^4.11.0",
+ "xterm-addon-fit": "^0.5.0",
+ "xterm-addon-search": "^0.8.0",
+ "xterm-addon-web-links": "^0.4.0"
},
"scripts": {
"linter": "yarn eslint --no-eslintrc -c .eslintrc.js --ext .js --ext .jsx .",
diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py
index d2bd1af6..6e1e4ea6 100644
--- a/web/pgAdmin4.py
+++ b/web/pgAdmin4.py
@@ -15,6 +15,7 @@ to start a web server."""
import sys
from cheroot.wsgi import Server as CherootServer
+
if sys.version_info < (3, 4):
raise RuntimeError('This application must be run under Python 3.4 '
'or later.')
@@ -37,7 +38,7 @@ else:
builtins.SERVER_MODE = None
import config
-from pgadmin import create_app
+from pgadmin import create_app, socketio
from pgadmin.utils import u_encode, fs_encoding, file_quote
from pgadmin.utils.constants import INTERNAL
# Get the config database schema version. We store this in pgadmin.model
@@ -97,6 +98,8 @@ if not os.path.isfile(config.SQLITE_PATH):
##########################################################################
app = create_app()
app.debug = False
+app.config['sessions'] = dict()
+
if config.SERVER_MODE:
app.wsgi_app = ReverseProxied(app.wsgi_app)
@@ -206,17 +209,16 @@ def main():
else:
# Can use cheroot instead of flask dev server when not in debug
# 10 is default thread count in CherootServer
- num_threads = 10 if config.THREADED_MODE else 1
- prod_server = CherootServer(
- (config.DEFAULT_SERVER, config.EFFECTIVE_SERVER_PORT),
- wsgi_app=app,
- numthreads=num_threads,
- server_name=config.APP_NAME)
+ # num_threads = 10 if config.THREADED_MODE else 1
try:
- print("Using production server...")
- prod_server.start()
+ socketio.run(
+ app,
+ host=config.DEFAULT_SERVER,
+ port=config.EFFECTIVE_SERVER_PORT,
+ )
except KeyboardInterrupt:
- prod_server.stop()
+ print("CLOSE SERVER")
+ socketio.stop()
except IOError:
app.logger.error("Error starting the app server: %s", sys.exc_info())
diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py
index a7333537..a641ea66 100644
--- a/web/pgadmin/__init__.py
+++ b/web/pgadmin/__init__.py
@@ -19,6 +19,7 @@ from collections import defaultdict
from importlib import import_module
from flask import Flask, abort, request, current_app, session, url_for
+from flask_socketio import SocketIO
from werkzeug.exceptions import HTTPException
from flask_babelex import Babel, gettext
from flask_babelex import gettext as _
@@ -52,10 +53,15 @@ import mimetypes
mimetypes.add_type('application/javascript', '.js')
mimetypes.add_type('text/css', '.css')
+
winreg = None
if os.name == 'nt':
import winreg
+socketio = SocketIO(manage_session=False, async_mode='eventlet',
+ logger=True, engineio_logger=True, debug=False,
+ ping_interval=25, ping_timeout=120)
+
class PgAdmin(Flask):
def __init__(self, *args, **kwargs):
@@ -811,4 +817,5 @@ def create_app(app_name=None):
##########################################################################
# All done!
##########################################################################
+ socketio.init_app(app)
return app
diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py
index 235db027..b093e3a4 100644
--- a/web/pgadmin/browser/register_browser_preferences.py
+++ b/web/pgadmin/browser/register_browser_preferences.py
@@ -10,6 +10,7 @@ from flask_babelex import gettext
from pgadmin.utils.constants import PREF_LABEL_DISPLAY,\
PREF_LABEL_KEYBOARD_SHORTCUTS, PREF_LABEL_TABS_SETTINGS, \
PREF_LABEL_OPTIONS
+from flask_security import current_user
import config
LOCK_LAYOUT_LEVEL = {
@@ -511,10 +512,12 @@ def register_browser_preferences(self):
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, or Schema Diff from '
- 'the drop-down to set open in new browser tab for '
- 'that particular module.'),
+ {'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,
diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
index 01ab89c5..c4784d51 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
+++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.js
@@ -91,6 +91,11 @@ define('pgadmin.node.database', [
name: 'generate_erd', node: 'database', module: this,
applies: ['object', 'context'], callback: 'generate_erd',
category: 'erd', priority: 5, label: gettext('Generate ERD (Beta)'),
+ },{
+ name: 'psql_tool', node: 'database', module: this,
+ applies: ['object', 'context'], callback: 'db_psql_tool',
+ category: 'psql_tool', priority: 5, label: gettext('PSQL Tool (Beta)'),
+ enable: 'is_psql_enabled'
}]);
_.bindAll(this, 'connection_lost');
@@ -122,6 +127,9 @@ define('pgadmin.node.database', [
is_connected: function(node) {
return (node && node.connected == true && node.canDisconn == true);
},
+ is_psql_enabled: function(node) {
+ return (node && node.connected == true) && pgAdmin['enable_psql'];
+ },
is_conn_allow: function(node) {
return (node && node.allowConn == true);
},
@@ -266,6 +274,15 @@ define('pgadmin.node.database', [
pgBrowser.erd.showErdTool(d, i, true);
},
+ /* Open psql tool for db*/
+ db_psql_tool: function(args) {
+ var input = args || {},
+ t = pgBrowser.tree,
+ i = input.item || t.selected(),
+ d = i && i.length == 1 ? t.itemData(i) : undefined;
+ pgBrowser.psql.psql_tool(d, i, true);
+ },
+
/* Connect the database (if not connected), before opening this node */
beforeopen: function(item, data) {
if(!data || data._type != 'database' || data.label == 'template0') {
diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js
index b21cba43..1a7a0cef 100644
--- a/web/pgadmin/browser/server_groups/servers/static/js/server.js
+++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js
@@ -102,6 +102,12 @@ define('pgadmin.node.server', [
data_disabled: gettext('Database is already disconnected.'),
},
},{
+ name: 'server_psql', node: 'server', module: this,
+ applies: ['object', 'context'], callback: 'server_psql_tool',
+ category: 'psql_tool', priority: 5, label: gettext('PSQL Tool (Beta)'),
+ enable : 'is_psql_enabled',
+ },
+ {
name: 'reload_configuration', node: 'server', module: this,
applies: ['tools', 'context'], callback: 'reload_configuration',
category: 'reload', priority: 6, label: gettext('Reload Configuration'),
@@ -183,6 +189,9 @@ define('pgadmin.node.server', [
is_connected: function(node) {
return (node && node.connected == true);
},
+ is_psql_enabled: function(node) {
+ return (node && node.connected == true) && pgAdmin['enable_psql'];
+ },
enable_reload_config: function(node) {
// Must be connected & is Super user
if (node && node._type == 'server' &&
@@ -728,6 +737,14 @@ define('pgadmin.node.server', [
return false;
},
+ /* Open psql tool for server*/
+ server_psql_tool: function(args) {
+ var input = args || {},
+ t = pgBrowser.tree,
+ i = input.item || t.selected(),
+ d = i && i.length == 1 ? t.itemData(i) : undefined;
+ pgBrowser.psql.psql_tool(d, i, true);
+ }
},
model: pgAdmin.Browser.Node.Model.extend({
defaults: {
diff --git a/web/pgadmin/browser/static/js/panel.js b/web/pgadmin/browser/static/js/panel.js
index c9e64132..3068aabe 100644
--- a/web/pgadmin/browser/static/js/panel.js
+++ b/web/pgadmin/browser/static/js/panel.js
@@ -122,10 +122,30 @@ define(
myPanel.on(ev, that.handleVisibility.bind(myPanel, ev));
});
}
+
+ // Listen on detach panel event
+ myPanel.on(wcDocker.EVENT.DETACHED, function(obj) {
+ that.setCodeMirrorHeight(obj);
+ });
},
});
}
},
+ setCodeMirrorHeight: function() {
+ // Fix for mac os code-mirror showing black screen.
+ var txtArea = $('.pg-panel-content .sql_textarea > textarea').first();
+ txtArea.css('z-index', -1);
+ var $tabContent = $('.pg-panel-content > .sql_textarea').first();
+ var $sqlPane = $tabContent.find('.CodeMirror > div > textarea');
+ for(let i=0; i<$sqlPane.length; i++) {$($sqlPane[i]).css('z-index', -1);}
+
+ $tabContent = $('.pg-panel-content > .sql_textarea').first();
+ $sqlPane = $tabContent.find('.pg-panel-content');
+ $sqlPane.find('.CodeMirror').css(
+ 'cssText',
+ 'height: ' + ($tabContent.height()) + 'px !important;'
+ );
+ },
eventFunc: function(eventName) {
var name = $(this).data('pgAdminName');
diff --git a/web/pgadmin/browser/templates/browser/js/utils.js b/web/pgadmin/browser/templates/browser/js/utils.js
index 8597df48..b0a317a5 100644
--- a/web/pgadmin/browser/templates/browser/js/utils.js
+++ b/web/pgadmin/browser/templates/browser/js/utils.js
@@ -52,6 +52,10 @@ define('pgadmin.browser.utils',
pgAdmin['user_inactivity_timeout'] = {{ current_app.config.get('USER_INACTIVITY_TIMEOUT') }};
pgAdmin['override_user_inactivity_timeout'] = '{{ current_app.config.get('OVERRIDE_USER_INACTIVITY_TIMEOUT') }}' == 'True';
+ /* GET PSQL Tool related config */
+ pgAdmin['enable_psql'] = '{{ current_app.config.get('ENABLE_PSQL') }}' == 'True';
+ pgAdmin['allow_psql_shell_commands'] = '{{ current_app.config.get('ALLOW_PSQL_SHELL_COMMANDS') }}' == 'True';
+
// Define list of nodes on which Query tool option doesn't appears
var unsupported_nodes = pgAdmin.unsupported_nodes = [
'server_group', 'server', 'coll-tablespace', 'tablespace',
diff --git a/web/pgadmin/static/bundle/browser.js b/web/pgadmin/static/bundle/browser.js
index 14140a2f..0b3ad81b 100644
--- a/web/pgadmin/static/bundle/browser.js
+++ b/web/pgadmin/static/bundle/browser.js
@@ -11,6 +11,7 @@ define('bundled_browser',[
'pgadmin.browser',
'sources/browser/index',
'top/tools/erd/static/js/index',
+ 'top/tools/psql/static/js/index',
], function(pgBrowser) {
pgBrowser.init();
});
diff --git a/web/pgadmin/static/css/style.css b/web/pgadmin/static/css/style.css
index c2a776c8..5b4a9f2d 100644
--- a/web/pgadmin/static/css/style.css
+++ b/web/pgadmin/static/css/style.css
@@ -21,3 +21,5 @@
@import '../vendor/backgrid/backgrid.css';
@import '../vendor/backgrid/backgrid-select-all.css';
+
+@import 'node_modules/xterm/css/xterm.css';
diff --git a/web/pgadmin/tools/psql/__init__.py b/web/pgadmin/tools/psql/__init__.py
new file mode 100644
index 00000000..27ae7e2a
--- /dev/null
+++ b/web/pgadmin/tools/psql/__init__.py
@@ -0,0 +1,503 @@
+#!/usr/bin/env python3
+import os
+import re
+import select
+import signal
+import termios
+import struct
+import fcntl
+import pty
+import config
+import eventlet.green.subprocess as subprocess
+from flask import Response, session, url_for, request
+from pgadmin.browser.utils import underscore_unescape
+from flask import render_template, copy_current_request_context, \
+ current_app as app
+from flask_babelex import gettext
+from pgadmin.utils import PgAdminModule
+from flask_security import login_required, current_user
+from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS, \
+ ERROR_MSG_TRANS_ID_NOT_FOUND
+from pgadmin.utils.driver import get_driver
+from config import PG_DEFAULT_DRIVER
+from pgadmin.model import Server
+from pgadmin.utils import get_complete_file_path
+
+from ... import socketio as sio
+
+session_input = dict()
+session_input_cursor = dict()
+session_last_cmd = dict()
+pdata = dict()
+cdata = dict()
+
+
+class PSQLModule(PgAdminModule):
+ """
+ class PSQLModule(PgAdminModule)
+ A module class for PSQL derived from PgAdminModule.
+ """
+
+ LABEL = gettext("PSQL")
+
+ def get_own_menuitems(self):
+ return {}
+
+ def get_own_javascripts(self):
+ return [{
+ 'name': 'pgadmin.psql',
+ 'path': url_for('psql.index') + "psql",
+ 'when': None
+ }]
+
+ def get_panels(self):
+ return []
+
+ def get_exposed_url_endpoints(self):
+ """
+ Returns:
+ list: URL endpoints for PSQL module
+ """
+ return [
+ 'psql.panel',
+ ]
+
+
+blueprint = PSQLModule('psql', __name__, static_url_path='/static')
+
+
[email protected]("/psql.js")
+@login_required
+def script():
+ """render the required javascript"""
+ return Response(
+ response=render_template("psql/js/psql.js", _=gettext),
+ status=200,
+ mimetype=MIMETYPE_APP_JS
+ )
+
+
[email protected]('/panel/<int:trans_id>',
+ methods=["POST"],
+ endpoint="panel")
+@login_required
+def panel(trans_id):
+ """
+ Return panel template for PSQL tools.
+ :param trans_id:
+ """
+ params = {
+ 'trans_id': trans_id,
+ 'title': request.form['title']
+ }
+ if request.args:
+ params.update({k: v for k, v in request.args.items()})
+
+ return render_template('editor_template.html',
+ sid=params['sid'],
+ db=params['db'],
+ server_type=params['server_type'],
+ is_enable=config.ENABLE_PSQL,
+ title=underscore_unescape(params['title']))
+
+
+def set_term_size(fd, row, col, xpix=0, ypix=0):
+ """
+ Set the terminal size as per UI xterm size.
+ :param fd:
+ :param row:
+ :param col:
+ :param xpix:
+ :param ypix:
+ """
+ term_size = struct.pack('HHHH', row, col, xpix, ypix)
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, term_size)
+
+
[email protected]('connect', namespace='/pty')
+def connect():
+ """
+ Connect to the server through socket.
+ :return:
+ :rtype:
+ """
+ if config.ENABLE_PSQL:
+ sio.emit('connected', {'sid': request.sid}, namespace='/pty',
+ to=request.sid)
+ if request.sid in session_last_cmd:
+ session_last_cmd[request.sid]['is_new_connection'] = False
+ else:
+ session_last_cmd[request.sid] = {'cmd': '', 'arrow_up': False,
+ 'invalid_cmd': False,
+ 'is_new_connection': True}
+ else:
+ sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty',
+ to=request.sid)
+
+
[email protected]('start_process', namespace='/pty')
+@login_required
+def start_process(data):
+ """
+ Start the pty terminal and execute psql command and emit results to user.
+ :param data:
+ :return:
+ """
+
+ @copy_current_request_context
+ def read_and_forward_pty_output(sid, data):
+ max_read_bytes = 1024 * 20
+ # Create the pty terminal process, parent and fd are file descriptors
+ # for parent and child.
+ parent, fd = pty.openpty()
+ p = None
+ if parent is not None:
+ # Child process
+ p = subprocess.Popen(connection_data,
+ preexec_fn=os.setsid,
+ stdin=fd,
+ stdout=fd,
+ stderr=fd,
+ universal_newlines=True
+ )
+
+ app.config['sessions'][request.sid] = parent
+ pdata[request.sid] = p
+ cdata[request.sid] = fd
+ else:
+ app.config['sessions'][request.sid] = parent
+ cdata[request.sid] = fd
+ set_term_size(fd, 50, 50)
+
+ while p and p.poll() is None:
+ if request.sid in app.config['sessions']:
+ # This code is added to make this unit testable.
+ if "is_test" not in data:
+ sio.sleep(0.01)
+ else:
+ data['count'] += 1
+ if data['count'] == 5:
+ break
+
+ timeout = 0
+ # module provides access to platform-specific I/O
+ # monitoring functions
+ (data_ready, _, _) = select.select([parent, fd], [], [],
+ timeout)
+
+ if parent in data_ready:
+ # Read the output from parent fd (terminal).
+ output = os.read(parent, max_read_bytes)
+ emit_output = True
+
+ if sid in session_last_cmd and session_last_cmd[sid][
+ 'arrow_up'] and not session_last_cmd[request.sid][
+ 'arrow_left_right']:
+ session_last_cmd[sid]['cmd'] = output.decode()
+ session_input_cursor[request.sid] = len(
+ session_last_cmd[sid]['cmd'])
+ session_last_cmd[sid]['arrow_up'] = True
+
+ if sid in session_last_cmd:
+ # If command is invalid then emit error to user.
+ if session_last_cmd[sid]['invalid_cmd']:
+ emit_output = False
+ sio.emit(
+ 'pty-output',
+ {
+ 'result': gettext(
+ "ERROR: Shell commands are disabled "
+ "in psql for security;\r\n"),
+ 'error': True
+ },
+ namespace='/pty', room=sid)
+ # If command is valid then emit output to user.
+ if emit_output:
+ sio.emit('pty-output',
+ {'result': output.decode(),
+ 'error': False},
+ namespace='/pty', room=sid)
+ else:
+ session_last_cmd[request.sid]['invalid_cmd'] = False
+
+ # Check user is authenticated and PSQL is enabled in config.
+ if current_user.is_authenticated and config.ENABLE_PSQL:
+ connection_data = []
+ try:
+ db = ''
+ if data['db']:
+ db = data['db']
+ # driver = get_driver(PG_DEFAULT_DRIVER)
+ # manager = driver.connection_manager(int(data['sid']))
+ # conn = manager.connection()
+ conn, manager = _get_connection(int(data['sid']), data)
+ psql_utility = manager.utility('sql')
+ print("psql_utility: ", psql_utility)
+ connection_data = get_connection_str(psql_utility, conn, db,
+ manager)
+ except Exception as e:
+ # If any error raised during the start the PSQL emit error to UI.
+ sio.emit(
+ 'conn_error',
+ {
+ 'error': 'Error while running psql command: {0}'.format(e),
+ }, namespace='/pty', room=request.sid)
+
+ try:
+ sio.start_background_task(read_and_forward_pty_output,
+ request.sid, data)
+ except Exception as e:
+ print(e)
+ else:
+ # Show error if user is not authenticated.
+ sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty',
+ to=request.sid)
+
+
+def _get_connection(sid, data):
+ """
+ Get the connection object of ERD.
+ :param sid:
+ :param did:
+ :param trans_id:
+ :return:
+ """
+ manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
+ try:
+ conn = manager.connection()
+ if 'pwd' in data:
+ kwargs = {'password': data['pwd'], "user": data['user']}
+ status, msg = conn.connect(**kwargs)
+ else:
+ status, msg = conn.connect()
+ if not status:
+ app.logger.error(msg)
+ # raise ConnectionLost(sid, conn.db, trans_id)
+
+ return conn, manager
+ except Exception as e:
+ app.logger.error(e)
+ raise
+
+
+def get_connection_str(psql_utility, conn, db, manager):
+ """
+ Get connection string(through connection dsn)
+ :param psql_utility: PostgreSQL binary path.
+ :param conn: Connection data
+ :param db: database name to connect specific db.
+ :return: connection attribute list for PSQL connection.
+ """
+
+ conn_attr = conn.conn.dsn
+ if 'password=xxx' in conn_attr:
+ conn_attr = conn_attr.replace('password=xxx', '')
+
+ if db != '':
+ conn_attr = conn_attr.replace('dbname=postgres',
+ 'dbname={0}'.format(db))
+ # Add application name to new created psql terminal instance
+ # through pgAdmin 4.
+ conn_attr = "{0} {1}".format(conn_attr,
+ " application_name='pgAdmin4: psql'")
+
+ conn_attr_list = list()
+ conn_attr_list.append(psql_utility)
+ conn_attr_list.append(conn_attr)
+ return conn_attr_list
+
+
[email protected]('socket_input', namespace='/pty')
+def socket_input(data):
+ """
+ This get the user input through socket.
+ :param data: User input from socket.
+ """
+ try:
+ # Check PSQL enabled setting from config.
+ enable_psql = True if config.ENABLE_PSQL else False
+
+ if request.sid in app.config['sessions']:
+ if data['key_name'] == 'Enter' and enable_psql:
+ # If user get previous executed command from history then set
+ # current command as previous executed command.
+ if session_last_cmd[request.sid]['cmd'] \
+ and session_last_cmd[request.sid]['arrow_up']:
+ user_input = str(
+ session_last_cmd[request.sid]['cmd']).strip()
+ session_last_cmd[request.sid]['arrow_up'] = False
+ session_last_cmd[request.sid]['cmd'] = ''
+ else:
+ user_input = str(session_input[request.sid]).strip()
+ session_input_cursor[request.sid] = 0
+
+ # If ALLOW_PSQL_SHELL_COMMANDS is False then user can't execute
+ # \! meta command to run shell commands through PSQL terminal.
+ # Check before executing the user entered command does not
+ # contains \! in input.
+ is_new_connection = session_last_cmd[request.sid][
+ 'is_new_connection']
+ if user_input.startswith('\\!') and \
+ re.match("^\\\!$", user_input) and \
+ len(user_input) == 2 and \
+ not config.ALLOW_PSQL_SHELL_COMMANDS \
+ and not is_new_connection:
+
+ 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())
+
+ os.write(app.config['sessions'][request.sid],
+ '\n'.encode())
+ session_input[request.sid] = ''
+ elif re.search("\\\!", user_input) and \
+ not config.ALLOW_PSQL_SHELL_COMMANDS and \
+ not session_last_cmd[request.sid]['is_new_connection']:
+ stop_execution = True
+ # Check \! is passed as string or not.
+ double_quote_strs = re.findall('"([^"]*)"', user_input)
+
+ if double_quote_strs:
+ for sub_str in double_quote_strs:
+ if re.search("\\\!", sub_str):
+ stop_execution = False
+ # break
+
+ if stop_execution:
+ session_last_cmd[request.sid]['invalid_cmd'] = True
+ # 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],
+ '\n'.encode())
+ else:
+ session_last_cmd[request.sid]['invalid_cmd'] = False
+ os.write(app.config['sessions'][request.sid],
+ '\n'.encode())
+ elif user_input == '\q':
+ # If user enter \q to terminate the PSQL, emit the msg to
+ # notify user connection is terminated.
+ os.write(app.config['sessions'][request.sid],
+ '\n'.encode())
+ sio.emit('pty-output',
+ {
+ 'result': gettext(
+ 'Connection terminated, TO create new '
+ 'connection please open another psql'
+ ' tool.'),
+ 'error': True},
+ namespace='/pty', room=request.sid)
+ else:
+ os.write(app.config['sessions'][request.sid],
+ data['input'].encode())
+ session_input[request.sid] = ''
+ session_last_cmd[request.sid]['is_new_connection'] = False
+ else:
+ if data['key_name'] == 'ArrowLeft':
+ session_last_cmd[request.sid]['arrow_left_right'] = True
+ if session_input_cursor[request.sid] > 0:
+ session_input_cursor[request.sid] -= 1
+
+ elif data['key_name'] == 'ArrowRight':
+ session_last_cmd[request.sid]['arrow_left_right'] = True
+ if session_input_cursor[request.sid] < len(
+ session_input[request.sid]):
+ session_input_cursor[request.sid] += 1
+
+ elif data['key_name'] == 'ArrowUp':
+ session_last_cmd[request.sid]['arrow_up'] = True
+ session_last_cmd[request.sid]['arrow_left_right'] = False
+ session_input[request.sid] = session_last_cmd[request.sid][
+ 'cmd']
+ session_input_cursor[request.sid] = len(
+ session_last_cmd[request.sid]['cmd'])
+
+ elif request.sid in session_input and data[
+ 'key_name'] == 'Backspace' and (
+ len(session_input[request.sid]) or len(
+ session_last_cmd[request.sid])):
+
+ session_last_cmd[request.sid]['arrow_left_right'] = True
+
+ if session_last_cmd[request.sid]['cmd']:
+ session_input[request.sid] = \
+ session_last_cmd[request.sid]['cmd']
+
+ user_input = list(session_input[request.sid])
+
+ if session_input_cursor[request.sid] == 1:
+ index = 0
+ session_input_cursor[request.sid] -= 1
+ else:
+ if session_input_cursor[request.sid] > 0:
+ index = (session_input_cursor[request.sid]) - 1
+ session_input_cursor[request.sid] -= 1
+ else:
+ index = session_input_cursor[request.sid]
+ session_input_cursor[request.sid] = 0
+
+ if len(user_input):
+ del user_input[index]
+ session_input[request.sid] = "".join(user_input)
+
+ if len(session_input[request.sid]) == 0:
+ session_input_cursor[request.sid] = 0
+ session_last_cmd[request.sid]['cmd'] = ''
+ elif request.sid in session_input:
+ if session_last_cmd[request.sid]['cmd'] and session_input[
+ request.sid] == '':
+ session_input[request.sid] = \
+ session_last_cmd[request.sid]['cmd']
+ session_input_cursor[request.sid] = len(
+ session_input[request.sid])
+ else:
+ session_last_cmd[request.sid]['arrow_up'] = False
+ session_last_cmd[request.sid]['cmd'] = ''
+ user_input = list(session_input[request.sid])
+ user_input.insert(session_input_cursor[request.sid],
+ data['input'])
+ session_input[request.sid] = ''.join(user_input)
+ session_input_cursor[request.sid] += 1
+ session_last_cmd[request.sid]['arrow_left_right'] = False
+ else:
+ session_input_cursor[request.sid] = 0
+ 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())
+ except Exception as e:
+ # Delete socket id from sessions.
+ del app.config['sessions'][request.sid]
+
+
[email protected]('resize', namespace='/pty')
+def resize(data):
+ """
+ Resize the pty terminal as per the UI terminal.
+ :param data: UI terminal rows and cols data
+ """
+ if request.sid in app.config['sessions']:
+ set_term_size(app.config['sessions'][request.sid], data['rows'],
+ data['cols'])
+
+
[email protected]('disconnect', namespace='/pty')
+def disconnect():
+ """
+ Disconnect the socket and terminate the process
+ """
+ if request.sid in pdata:
+ # On disconnect socket manually exit the psql terminal and close the
+ # parend and child fd then kill the subprocess.
+ 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]
+ os.kill(pdata[request.sid].pid, signal.SIGSTOP)
diff --git a/web/pgadmin/tools/psql/static/js/index.js b/web/pgadmin/tools/psql/static/js/index.js
new file mode 100644
index 00000000..4d88ba04
--- /dev/null
+++ b/web/pgadmin/tools/psql/static/js/index.js
@@ -0,0 +1,23 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2021, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from 'sources/gettext';
+import url_for from 'sources/url_for';
+import $ from 'jquery';
+import _ from 'underscore';
+import pgAdmin from 'sources/pgadmin';
+import pgBrowser from 'top/browser/static/js/browser';
+import * as csrfToken from 'sources/csrf';
+import {initialize} from './psql_module';
+
+let pgBrowserOut = initialize(gettext, url_for, $, _, pgAdmin, csrfToken, pgBrowser);
+
+module.exports = {
+ pgBrowser: pgBrowserOut,
+};
diff --git a/web/pgadmin/tools/psql/static/js/psql_module.js b/web/pgadmin/tools/psql/static/js/psql_module.js
new file mode 100644
index 00000000..a9bdd90b
--- /dev/null
+++ b/web/pgadmin/tools/psql/static/js/psql_module.js
@@ -0,0 +1,329 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2021, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import { Terminal } from 'xterm';
+import { FitAddon } from 'xterm-addon-fit';
+import { WebLinksAddon } from 'xterm-addon-web-links';
+import { SearchAddon } from 'xterm-addon-search';
+import { io } from 'socketio';
+import Alertify from 'pgadmin.alertifyjs';
+import clipboard from 'sources/selection/clipboard';
+
+import 'wcdocker';
+import {getRandomInt} from 'sources/utils';
+import {getTreeNodeHierarchyFromIdentifier} from 'sources/tree/pgadmin_tree_node';
+
+
+export function setPanelTitle(psqlToolPanel, panelTitle) {
+ psqlToolPanel.title('<span title="'+panelTitle+'">'+panelTitle+'</span>');
+}
+
+var wcDocker = window.wcDocker;
+
+export function initialize(gettext, url_for, $, _, pgAdmin, csrfToken, Browser) {
+ var pgBrowser = Browser;
+ var terminal = Terminal;
+ var parentData = null;
+ /* Return back, this has been called more than once */
+ if (pgBrowser.psql)
+ return pgBrowser.psql;
+
+ // Create an Object Restore of pgBrowser class
+ pgBrowser.psql = {
+ init: function() {
+ this.initialized = true;
+
+ csrfToken.setPGCSRFToken(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
+ // Define the nodes on which the menus to be appear
+ var menus = [{
+ name: 'psql',
+ module: this,
+ applies: ['tools'],
+ callback: 'psql_tool',
+ priority: 1,
+ label: gettext('PSQL Tool (Beta)'),
+ enable: this.psqlToolEnabled,
+ }];
+
+ this.enable_psql_tool = pgAdmin['enable_psql'];
+ if(pgAdmin['enable_psql']) {
+ pgBrowser.add_menus(menus);
+ }
+
+ // Creating a new pgBrowser frame to show the data.
+ var psqlFrameType = new pgBrowser.Frame({
+ name: 'frm_psqltool',
+ showTitle: true,
+ isCloseable: true,
+ isPrivate: true,
+ url: 'about:blank',
+ });
+
+ let self = this;
+ /* Cache may take time to load for the first time
+ * Keep trying till available
+ */
+ let cacheIntervalId = setInterval(function() {
+ if(pgBrowser.preference_version() > 0) {
+ self.preferences = pgBrowser.get_preferences_for_module('psql');
+ clearInterval(cacheIntervalId);
+ }
+ },0);
+
+ pgBrowser.onPreferencesChange('psql', function() {
+ self.preferences = pgBrowser.get_preferences_for_module('psql');
+ });
+
+ // Load the newly created frame
+ psqlFrameType.load(pgBrowser.docker);
+ return this;
+ },
+
+ psqlToolEnabled: function(obj) {
+ //Same as query tool
+ var isEnabled = (() => {
+ if (!_.isUndefined(obj) && !_.isNull(obj)) {
+ if ((this.enable_psql_tool == true) && ((obj._type == 'server' && obj.connected == true )|| obj._type == 'database')) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ })();
+ return isEnabled;
+ },
+ retrieveAncestorOfTypeServer: function(item) {
+ let serverInformation = null;
+ // let aciTreeItem = item || pgBrowser.treeMenu.selected();
+ let treeNode = pgBrowser.treeMenu.findNodeByDomElement(item);
+
+ if (treeNode) {
+ let nodeData;
+ let databaseNode = treeNode.ancestorNode(
+ (node) => {
+ nodeData = node.getData();
+ return (nodeData._type === 'database');
+ }
+ );
+ let isServerNode = (node) => {
+ nodeData = node.getData();
+ return nodeData._type === 'server';
+ };
+
+ if (databaseNode !== null) {
+ if (nodeData._label.indexOf('=') >= 0) {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext(
+ 'Databases with = symbols in the name cannot be backed up or restored using this utility.'
+ )
+ );
+ } else {
+ if (databaseNode.anyParent(isServerNode))
+ serverInformation = nodeData;
+ }
+ } else {
+ if (treeNode.anyFamilyMember(isServerNode))
+ serverInformation = nodeData;
+ }
+ }
+
+ if (serverInformation === null) {
+ this.alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Please select server or child node from the browser tree.')
+ );
+ }
+ return serverInformation;
+ },
+ psql_tool: function(data, aciTreeIdentifier, gen=false) {
+ const module = 'paths';
+ let preference_name = 'pg_bin_dir';
+ let msg = gettext('Please configure the PostgreSQL Binary Path in the Preferences dialog.');
+ const serverInformation = this.retrieveAncestorOfTypeServer(aciTreeIdentifier);
+
+ // const node1 = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if ((serverInformation.type && serverInformation.type === 'ppas') ||
+ serverInformation.server_type === 'ppas') {
+ preference_name = 'ppas_bin_dir';
+ msg = gettext('Please configure the EDB Advanced Server Binary Path in the Preferences dialog.');
+ }
+ const preference = pgBrowser.get_preference(module, preference_name);
+
+ if (preference) {
+ if (!preference.value) {
+ Alertify.alert(gettext('Configuration required'), msg);
+ return false;
+ }
+ } else {
+ Alertify.alert(
+ gettext(this.errorAlertTitle),
+ gettext('Failed to load preference %s of module %s', preference_name, module)
+ );
+ return false;
+ }
+ const node = pgBrowser.treeMenu.findNodeByDomElement(aciTreeIdentifier);
+ if (node === undefined || !node.getData()) {
+ Alertify.alert(
+ gettext('PSQL Error'),
+ gettext('No object selected.')
+ );
+ return;
+ }
+
+ parentData = getTreeNodeHierarchyFromIdentifier.call(
+ pgBrowser,
+ aciTreeIdentifier
+ );
+
+ if(_.isUndefined(parentData.server)) {
+ Alertify.alert(
+ gettext('PSQL Error'),
+ gettext('Please select a server/database object.')
+ );
+ return;
+ }
+
+ const transId = getRandomInt(1, 9999999);
+
+ var panelTitle = '';
+ if (parentData.database) {
+ panelTitle = parentData.database.label + '/' + parentData.server.user_name + '@' + parentData.server.label;
+ } else {
+ panelTitle = parentData.server.user_name + '@' + parentData.server.label;
+ }
+ const [panelUrl, panelCloseUrl] = this.getPanelUrls(transId, panelTitle, parentData, gen);
+
+ let psqlToolForm = `
+ <form id="psqlToolForm" action="${panelUrl}" method="post">
+ <input id="title" name="title" hidden />
+ <input name="close_url" value="${panelCloseUrl}" hidden />
+ </form>
+ <script>
+ document.getElementById("title").value = "${_.escape(panelTitle)}";
+ document.getElementById("psqlToolForm").submit();
+ </script>
+ `;
+
+ var open_new_tab = pgBrowser.get_preferences_for_module('browser').new_browser_tab_open;
+ if (open_new_tab && open_new_tab.includes('psql_tool')) {
+ var newWin = window.open('', '_blank');
+ newWin.document.write(psqlToolForm);
+ newWin.document.title = panelTitle;
+ } else {
+ /* On successfully initialization find the properties panel,
+ * create new panel and add it to the dashboard panel.
+ */
+ var propertiesPanel = pgBrowser.docker.findPanels('properties');
+ var psqlToolPanel = pgBrowser.docker.addPanel('frm_psqltool', wcDocker.DOCK.STACKED, propertiesPanel[0]);
+
+ // Set panel title and icon
+ setPanelTitle(psqlToolPanel, panelTitle);
+ psqlToolPanel.icon('fas fa-terminal');
+ psqlToolPanel.focus();
+
+ // Listen on the panel closed event.
+ /*psqlToolPanel.on(wcDocker.EVENT.CLOSED, function() {
+ $.ajax({
+ url: panelCloseUrl,
+ method: 'DELETE',
+ });
+ });*/
+
+ var openPSQLToolURL = function(j) {
+ // add spinner element
+ let $spinner_el =
+ $(`<div class="pg-sp-container">
+ <div class="pg-sp-content">
+ <div class="row">
+ <div class="col-12 pg-sp-icon"></div>
+ </div>
+ </div>
+ </div>`).appendTo($(j).data('embeddedFrame').$container);
+
+ let init_poller_id = setInterval(function() {
+ var frameInitialized = $(j).data('frameInitialized');
+ if (frameInitialized) {
+ clearInterval(init_poller_id);
+ var frame = $(j).data('embeddedFrame');
+ if (frame) {
+ frame.onLoaded(()=>{
+ $spinner_el.remove();
+ });
+ frame.openHTML(psqlToolForm);
+ }
+ }
+ }, 100);
+ };
+
+ openPSQLToolURL(psqlToolPanel);
+
+ }
+
+ // var url_params = {
+ // 'sid': parentData.server._id,
+ // };
+ // var baseUrl = url_for('psql.initialize', url_params);
+ //
+ // window.open(baseUrl, '_blank');
+ },
+ getPanelUrls: function(transId, panelTitle, parentData) {
+ let openUrl = url_for('psql.panel', {
+ trans_id: transId,
+ });
+
+ openUrl += `?sgid=${parentData.server_group._id}`
+ +`&sid=${parentData.server._id}`
+ +`&server_type=${parentData.server.server_type}`;
+
+ if(parentData.database && parentData.database._id) {
+ openUrl += `&db=${parentData.database._label}`;
+ } else {
+ openUrl += `&db=${''}`;
+ }
+
+ let closeUrl = url_for('psql.close', {
+ trans_id: transId,
+ // sgid: parentData.server_group._id,
+ // sid: parentData.server._id,
+ // did: parentData.database._id,
+ });
+ return [openUrl, closeUrl];
+ },
+ psql_terminal: function() {
+ return new terminal({
+ cursorBlink: true,
+ macOptionIsMeta: true,
+ scrollback: 10000,
+ });
+ },
+ psql_fit_screen: function() {
+ return new FitAddon();
+ },
+ psql_web_link: function() {
+ return new WebLinksAddon();
+ },
+ psql_search: function() {
+ return new SearchAddon();
+ },
+ psql_socket: function() {
+ return io('/pty', {pingTimeout: 120000, pingInterval: 25000});
+ },
+ get_parent_db: function(){
+ return parentData.server;
+ },
+ _clipboard: function() {
+ return clipboard;
+ }
+ };
+
+ return pgBrowser.psql;
+}
+
diff --git a/web/pgadmin/tools/psql/templates/editor_template.html b/web/pgadmin/tools/psql/templates/editor_template.html
new file mode 100644
index 00000000..e5f50a29
--- /dev/null
+++ b/web/pgadmin/tools/psql/templates/editor_template.html
@@ -0,0 +1,137 @@
+{% extends "base.html" %}
+{% block title %}{{title}}{% endblock %}
+
+{% block css_link %}
+<link type="text/css" rel="stylesheet" href="{{ url_for('browser.browser_css')}}"/>
+{% endblock %}
+{% block body %}
+<style>
+ body {padding: 0px;}
+ {% if is_desktop_mode and is_linux %}
+ .alertify .ajs-dimmer,.alertify .ajs-modal{-webkit-transform: none;}
+ .alertify-notifier{-webkit-transform: none;}
+ .alertify-notifier .ajs-message{-webkit-transform: none;}
+ .alertify .ajs-dialog.ajs-shake{-webkit-animation-name: none;}
+ .sql-editor-busy-icon.fa-pulse{-webkit-animation: none;}
+ {% endif %}
+</style>
+<div style="width: 100%; height: 100%;" id="psql-terminal" class="psql_terminal"></div>
+{% endblock %}
+
+
+{% block init_script %}
+require(
+ ['sources/generated/psql_tool'],
+ function(pgBrowser) {
+ const term = self.pgAdmin.Browser.psql.psql_terminal();
+
+ const fitAddon = self.pgAdmin.Browser.psql.psql_fit_screen();
+ term.loadAddon(fitAddon);
+ let clipboard = self.pgAdmin.Browser.psql._clipboard();
+
+ const webLinksAddon = self.pgAdmin.Browser.psql.psql_web_link();
+ term.loadAddon(webLinksAddon);
+
+ const searchAddon = self.pgAdmin.Browser.psql.psql_search();
+ term.loadAddon(searchAddon);
+
+ term.open(document.getElementById('psql-terminal'));
+ fitAddon.fit()
+ term.resize(15, 50)
+ fitAddon.fit()
+ let selected_text = '';
+ let user_input = '';
+ let is_pwd = true;
+ let cursor_position = 0;
+
+ term.attachCustomKeyEventHandler(e => {
+ e.stopPropagation();
+ if(e.type=='keydown' && e.metaKey &&(e.key == 'v' || e.key == 'V')) {
+ if(selected_text != '') {
+ if (selected_text.length > 0) {
+ socket.emit("socket_input", {"input": selected_text, 'key_name': e.code});
+ selected_text = '';
+ }
+ } else {
+ navigator.clipboard.readText().then( clipText => {
+ selected_text = clipText;
+ if (selected_text.length > 0) {
+ socket.emit("socket_input", {"input": selected_text, 'key_name': e.code});
+ selected_text = '';
+ }
+ });
+ }
+
+ }else if(e.type=='keydown' && e.metaKey && (e.key == 'c' || e.key == 'C')) {
+ if (term.hasSelection()) {
+ selected_text = term.getSelection();
+ } else {
+ selected_text = clipboard.readText();
+ }
+ }
+<!-- else if (e.type=='keydown' && e.keyCode == 32) {-->
+<!-- user_input += e.key;-->
+<!-- console.log('SPACEBAR');-->
+<!-- socket.emit("socket_input", {"input": e.key, 'key_name': e.code});-->
+<!-- }-->
+ return true;
+ });
+
+ term.onKey(function (ev) {
+ if (pgAdmin['allow_psql_shell_commands']) {
+ socket.emit("socket_input", {"input": ev.key, 'key_name': ev.domEvent.code});
+ } else {
+ console.log("socket_input" + ev.key);
+ socket.emit("socket_input", {"input": ev.key, 'key_name': ev.domEvent.code});
+ }
+ });
+
+ const socket = self.pgAdmin.Browser.psql.psql_socket();
+
+ socket.on("pty-output", function(data){
+ if(data.error) {
+ term.write('\r\n');
+ }
+ term.write(data.result);
+ if(data.error) {
+ term.write('\r\n');
+ }
+ })
+
+ socket.on("connect", () => {
+ if('{{is_enable}}' == 'True'){
+ socket.emit('start_process', {"sid": {{sid}}, "db": '{{db}}', 'stype': '{{server_type}}' });
+ }
+ fitToscreen();
+ });
+
+ socket.on("conn_error", (response) => {
+ term.write(response.error);
+ fitToscreen();
+ });
+
+ socket.on("conn_not_allow", () => {
+ term.write('PSQL connection not allowed');
+ fitToscreen();
+ });
+
+ 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 %}
+
+
diff --git a/web/pgadmin/tools/psql/tests/__init__.py b/web/pgadmin/tools/psql/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/web/pgadmin/tools/psql/tests/psql_test_data.json b/web/pgadmin/tools/psql/tests/psql_test_data.json
new file mode 100644
index 00000000..9b2cfaf5
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/psql_test_data.json
@@ -0,0 +1,174 @@
+{
+ "psql_user_input": [
+ {
+ "name": "Enter Select 1;",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "select 1;",
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ },
+ {
+ "name": "Enter Backspace",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "select 1;",
+ "is_backspace": true,
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ },{
+ "name": "Enter Backspace",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "select 1;",
+ "is_backspace": true,
+ "move_cursor_up": true,
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ },
+ {
+ "name": "Enter ArrowUp",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "select 1;",
+ "is_arrowUp": true,
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ },
+ {
+ "name": "Enter ArrowUp",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "select 1;",
+ "is_arrowUp": true,
+ "is_history": true,
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ },
+ {
+ "name": "Enter ArrowLeft",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "select 1;",
+ "is_arrowLeft": true,
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ },
+ {
+ "name": "Enter ArrowRight",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "select 1;",
+ "is_arrowRight": true,
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ },{
+ "name": "Read previous executed command",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "select 1;",
+ "is_arrowRight": true,
+ "move_cursor_right": true,
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ },
+ {
+ "name": "Meta command \\! not allowed",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "\\!",
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ },
+ {
+ "name": "Meta command \\! with other cmd not allowed",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "\\! ls",
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ },
+ {
+ "name": "Valid commands",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "select \"\\!\"",
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ },
+ {
+ "name": "Exist psql terminal by using \\q",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "\\q",
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ }
+ ],
+ "resize_terminal": [
+ {
+ "name": "Resize psql terminal as per UI.",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_data": {
+ "cols": 141,
+ "rows": 39
+ },
+ "mock_data": {
+
+ },
+ "expected_data": {
+ }
+ }
+ ],
+ "backend_task": [
+ {
+ "name": "Backend Task",
+ "is_positive_test": true,
+ "mocking_required": false,
+ "input_cmd": "Select 1;",
+ "is_backend_task": true,
+ "mock_data": {
+ "is_test": true
+ },
+ "expected_data": {
+ }
+ }
+ ]
+}
diff --git a/web/pgadmin/tools/psql/tests/test_backend_task.py b/web/pgadmin/tools/psql/tests/test_backend_task.py
new file mode 100644
index 00000000..99073c0a
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_backend_task.py
@@ -0,0 +1,88 @@
+import uuid
+import config
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from regression import parent_node_dict
+from regression.test_setup import config_data
+from pgadmin.utils import server_utils as server_utils
+from pgAdmin4 import app
+from . import utils as psql_utils
+from .... import socketio
+
+
+class PSQLBackend(BaseTestGenerator):
+ scenarios = utils.generate_scenarios('backend_task',
+ psql_utils.test_cases)
+
+ def setUp(self):
+ self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
+ database_info = parent_node_dict["database"][-1]
+ self.did = database_info["db_id"]
+ self.sid = parent_node_dict["server"][-1]["server_id"]
+ self.sgid = config_data["server_group"]
+ config.ENABLE_PSQL = True
+ self.server_con = server_utils.connect_server(self, self.sid)
+
+ def runTest(self):
+
+ # Fetch flask client to access current user and other cookies.
+ flask_client = app.test_client()
+ flask_client.get('/session')
+ self.test_client = socketio.test_client(app, namespace='/pty',
+ flask_test_client=flask_client)
+ self.assertTrue(self.test_client.is_connected('/pty'))
+ received = self.test_client.get_received('/pty')
+
+ assert received[0]['name'] == 'connected'
+ assert received[0]['args'][0]['sid'] != ''
+
+ data = {
+ 'sid': self.sid,
+ 'db': 'postgres',
+ 'pwd': self.server['db_password'],
+ 'user': self.server['username'],
+ 'is_test': True,
+ 'count': 0
+ }
+
+ self.test_client.emit('start_process', data, namespace='/pty')
+ self.test_client.get_received('/pty')
+
+ for p in self.server['db_password']:
+ input_data = {
+ 'input': p,
+ 'key_name': 'Key{0}'.format(p)
+ }
+ self.test_client.emit('socket_input', input_data, namespace='/pty')
+ self.test_client.get_received('/pty')
+
+ input_data = {
+ 'input': '\\n',
+ 'key_name': 'Enter'
+ }
+ self.test_client.emit('socket_input', input_data, namespace='/pty')
+ self.test_client.get_received('/pty')
+
+ for ip in self.input_cmd:
+ input_data = {
+ 'input': ip,
+ 'key_name': 'Key{0}'.format(ip)
+ }
+ self.test_client.emit('socket_input', input_data, namespace='/pty')
+ self.test_client.get_received('/pty')
+
+ input_data = {
+ 'input': '\\n',
+ 'key_name': 'Enter'
+ }
+ self.test_client.emit('socket_input', input_data, namespace='/pty')
+ self.test_client.get_received('/pty')
+ self.test_client.disconnect(namespace='/pty')
+
+ def tearDown(self):
+ connection = utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ utils.drop_database(connection, self.db_name)
diff --git a/web/pgadmin/tools/psql/tests/test_panel.py b/web/pgadmin/tools/psql/tests/test_panel.py
new file mode 100644
index 00000000..83480681
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_panel.py
@@ -0,0 +1,35 @@
+import uuid
+import random
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from regression import parent_node_dict
+from regression.test_setup import config_data
+
+
+class PSQLPanel(BaseTestGenerator):
+
+ def setUp(self):
+ self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
+ self.sid = parent_node_dict["server"][-1]["server_id"]
+ self.did = utils.create_database(self.server, self.db_name)
+ self.sgid = config_data["server_group"]
+
+ def runTest(self):
+ trans_id = random.randint(1, 9999999)
+ url = '/psql/panel/{trans_id}?sgid={sgid}&sid={sid}&server_type=pg' \
+ '&db={db_name}'.\
+ format(trans_id=trans_id, sgid=self.sgid, sid=self.sid,
+ db_name=self.db_name)
+
+ response = self.tester.post(
+ url, data={"title": "panel_title"},
+ content_type="application/x-www-form-urlencoded")
+ self.assertEqual(response.status_code, 200)
+
+ def tearDown(self):
+ connection = utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ utils.drop_database(connection, self.db_name)
diff --git a/web/pgadmin/tools/psql/tests/test_psql_disabled.py b/web/pgadmin/tools/psql/tests/test_psql_disabled.py
new file mode 100644
index 00000000..32eaa623
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_psql_disabled.py
@@ -0,0 +1,36 @@
+import uuid
+import config
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from regression import parent_node_dict
+from regression.test_setup import config_data
+# from flask import current_app as app
+
+from pgAdmin4 import app
+from .... import socketio
+
+
+class PSQLSocketDisabled(BaseTestGenerator):
+ def setUp(self):
+ self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
+ self.sid = parent_node_dict["server"][-1]["server_id"]
+ self.did = utils.create_database(self.server, self.db_name)
+ self.sgid = config_data["server_group"]
+ config.ENABLE_PSQL = False
+
+ def runTest(self):
+ self.test_client = socketio.test_client(app, namespace='/pty')
+ self.assertTrue(self.test_client.is_connected('/pty'))
+ received = self.test_client.get_received('/pty')
+ print("received:", received)
+ assert received[0]['name'] == 'conn_not_allow'
+ self.test_client.disconnect(namespace='/pty')
+ self.assertFalse(self.test_client.is_connected('/pty'))
+
+ def tearDown(self):
+ connection = utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ utils.drop_database(connection, self.db_name)
diff --git a/web/pgadmin/tools/psql/tests/test_psql_input.py b/web/pgadmin/tools/psql/tests/test_psql_input.py
new file mode 100644
index 00000000..8fe0b47a
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_psql_input.py
@@ -0,0 +1,159 @@
+import uuid
+import config
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from regression import parent_node_dict
+from regression.test_setup import config_data
+from pgadmin.utils import server_utils as server_utils
+from pgAdmin4 import app
+from . import utils as psql_utils
+from .... import socketio
+
+
+class PSQLInput(BaseTestGenerator):
+ scenarios = utils.generate_scenarios('psql_user_input',
+ psql_utils.test_cases)
+
+ def setUp(self):
+ self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
+ database_info = parent_node_dict["database"][-1]
+ self.did = database_info["db_id"]
+ self.sid = parent_node_dict["server"][-1]["server_id"]
+ self.sgid = config_data["server_group"]
+ config.ENABLE_PSQL = True
+ self.server_con = server_utils.connect_server(self, self.sid)
+
+ def runTest(self):
+
+ # Fetch flask client to access current user and other cookies.
+ flask_client = app.test_client()
+ flask_client.get('/session')
+ self.test_client = socketio.test_client(app, namespace='/pty',
+ flask_test_client=flask_client)
+ self.assertTrue(self.test_client.is_connected('/pty'))
+ received = self.test_client.get_received('/pty')
+
+ assert received[0]['name'] == 'connected'
+ assert received[0]['args'][0]['sid'] != ''
+
+ data = {
+ 'sid': self.sid,
+ 'db': 'postgres',
+ 'pwd': self.server['db_password'],
+ 'user': self.server['username']
+ }
+
+ self.test_client.emit('start_process', data, namespace='/pty')
+ received = self.test_client.get_received('/pty')
+ print("received values: ", received)
+
+ for p in self.server['db_password']:
+ input_data = {
+ 'input': p,
+ 'key_name': 'Key{0}'.format(p)
+ }
+ self.test_client.emit('socket_input', input_data, namespace='/pty')
+ received = self.test_client.get_received('/pty')
+ print("user I/P:: ", received)
+
+ input_data = {
+ 'input': '\\n',
+ 'key_name': 'Enter'
+ }
+ self.test_client.emit('socket_input', input_data, namespace='/pty')
+ received = self.test_client.get_received('/pty')
+
+ for ip in self.input_cmd:
+ input_data = {
+ 'input': ip,
+ 'key_name': 'Key{0}'.format(ip)
+ }
+ self.test_client.emit('socket_input', input_data, namespace='/pty')
+ self.test_client.get_received('/pty')
+
+ if hasattr(self, 'is_backspace') and self.is_backspace:
+ if hasattr(self, 'move_cursor_up') and self.move_cursor_up:
+ input_data = {
+ 'input': '',
+ 'key_name': 'ArrowUp'
+ }
+ self.test_client.emit('socket_input', input_data,
+ namespace='/pty')
+
+ for ip in self.input_cmd:
+ input_data = {
+ 'input': ip,
+ 'key_name': 'Backspace'
+ }
+ self.test_client.emit('socket_input', input_data,
+ namespace='/pty')
+ self.test_client.get_received('/pty')
+
+ if hasattr(self, 'is_arrowUp') and self.is_arrowUp:
+ if hasattr(self, 'is_history') and self.is_history:
+ for ip in self.input_cmd:
+ input_data = {
+ 'input': ip,
+ 'key_name': 'Key{0}'.format(ip)
+ }
+ self.test_client.emit('socket_input', input_data,
+ namespace='/pty')
+ self.test_client.get_received('/pty')
+
+ input_data = {
+ 'input': '',
+ 'key_name': 'ArrowUp'
+ }
+ self.test_client.emit('socket_input', input_data,
+ namespace='/pty')
+ self.test_client.get_received('/pty')
+
+ if hasattr(self, 'is_arrowLeft') and self.is_arrowLeft:
+ for ip in self.input_cmd:
+ input_data = {
+ 'input': ip,
+ 'key_name': 'ArrowLeft'
+ }
+ self.test_client.emit('socket_input', input_data,
+ namespace='/pty')
+ self.test_client.get_received('/pty')
+
+ if hasattr(self, 'is_arrowRight') and self.is_arrowRight:
+ for ip in self.input_cmd:
+ input_data = {
+ 'input': ip,
+ 'key_name': 'ArrowRight'
+ }
+ self.test_client.emit('socket_input', input_data,
+ namespace='/pty')
+ self.test_client.get_received('/pty')
+
+ if hasattr(self, 'move_cursor_right') and self.is_arrowRight:
+ for i in range(2):
+ input_data = {
+ 'input': '',
+ 'key_name': 'ArrowLeft'
+ }
+ self.test_client.emit('socket_input', input_data,
+ namespace='/pty')
+ input_data = {
+ 'input': '',
+ 'key_name': 'ArrowRight'
+ }
+ self.test_client.emit('socket_input', input_data,
+ namespace='/pty')
+
+ input_data = {
+ 'input': '\\n',
+ 'key_name': 'Enter'
+ }
+ self.test_client.emit('socket_input', input_data, namespace='/pty')
+ received = self.test_client.get_received('/pty')
+
+ def tearDown(self):
+ connection = utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ utils.drop_database(connection, self.db_name)
diff --git a/web/pgadmin/tools/psql/tests/test_resize_terminal.py b/web/pgadmin/tools/psql/tests/test_resize_terminal.py
new file mode 100644
index 00000000..9ce7d004
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_resize_terminal.py
@@ -0,0 +1,60 @@
+import uuid
+import config
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from regression import parent_node_dict
+from regression.test_setup import config_data
+from pgadmin.utils import server_utils as server_utils
+from pgAdmin4 import app
+from . import utils as psql_utils
+from .... import socketio
+
+
+class PSQLResizeTerminal(BaseTestGenerator):
+ scenarios = utils.generate_scenarios('resize_terminal',
+ psql_utils.test_cases)
+
+ def setUp(self):
+ self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
+ database_info = parent_node_dict["database"][-1]
+ self.did = database_info["db_id"]
+ self.sid = parent_node_dict["server"][-1]["server_id"]
+ self.sgid = config_data["server_group"]
+ config.ENABLE_PSQL = True
+ self.server_con = server_utils.connect_server(self, self.sid)
+
+ def runTest(self):
+
+ # Fetch flask client to access current user and other cookies.
+ flask_client = app.test_client()
+ flask_client.get('/session')
+ self.test_client = socketio.test_client(app, namespace='/pty',
+ flask_test_client=flask_client)
+ self.assertTrue(self.test_client.is_connected('/pty'))
+ received = self.test_client.get_received('/pty')
+
+ assert received[0]['name'] == 'connected'
+ assert received[0]['args'][0]['sid'] != ''
+
+ data = {
+ 'sid': self.sid,
+ 'db': 'postgres',
+ 'pwd': self.server['db_password'],
+ 'user': self.server['username']
+ }
+
+ self.test_client.emit('start_process', data, namespace='/pty')
+ received = self.test_client.get_received('/pty')
+
+ self.test_client.emit('resize', self.input_data, namespace='/pty')
+
+ # self.test_client.disconnect(namespace='/pty')
+ # self.assertFalse(self.test_client.is_connected('/pty'))
+
+ def tearDown(self):
+ connection = utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ utils.drop_database(connection, self.db_name)
diff --git a/web/pgadmin/tools/psql/tests/test_socket_connect.py b/web/pgadmin/tools/psql/tests/test_socket_connect.py
new file mode 100644
index 00000000..7b8031bd
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_socket_connect.py
@@ -0,0 +1,36 @@
+import uuid
+import config
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from regression import parent_node_dict
+from regression.test_setup import config_data
+# from flask import current_app as app
+from pgAdmin4 import app
+from .... import socketio
+
+
+class PSQLSocketConnect(BaseTestGenerator):
+ def setUp(self):
+ self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
+ self.sid = parent_node_dict["server"][-1]["server_id"]
+ self.did = utils.create_database(self.server, self.db_name)
+ self.sgid = config_data["server_group"]
+ config.ENABLE_PSQL = True
+
+ def runTest(self):
+ self.test_client = socketio.test_client(app, namespace='/pty')
+ self.assertTrue(self.test_client.is_connected('/pty'))
+ received = self.test_client.get_received('/pty')
+ print("received:", received)
+ assert received[0]['name'] == 'connected'
+ assert received[0]['args'][0]['sid'] != ''
+ self.test_client.disconnect(namespace='/pty')
+ self.assertFalse(self.test_client.is_connected('/pty'))
+
+ def tearDown(self):
+ connection = utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ utils.drop_database(connection, self.db_name)
diff --git a/web/pgadmin/tools/psql/tests/test_socket_disconnect.py b/web/pgadmin/tools/psql/tests/test_socket_disconnect.py
new file mode 100644
index 00000000..cd2cf5ac
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_socket_disconnect.py
@@ -0,0 +1,35 @@
+import uuid
+import config
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from regression import parent_node_dict
+from regression.test_setup import config_data
+# from flask import current_app as app
+from pgAdmin4 import app
+from .... import socketio
+
+
+class PSQLSocketDisconnect(BaseTestGenerator):
+ def setUp(self):
+ self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
+ self.sid = parent_node_dict["server"][-1]["server_id"]
+ self.did = utils.create_database(self.server, self.db_name)
+ self.sgid = config_data["server_group"]
+ config.ENABLE_PSQL = True
+
+ def runTest(self):
+ self.test_client = socketio.test_client(app, namespace='/pty')
+ self.assertTrue(self.test_client.is_connected('/pty'))
+ received = self.test_client.get_received('/pty')
+ print("received:", received)
+ assert received[0]['name'] == 'connected'
+ assert received[0]['args'][0]['sid'] != ''
+ self.test_client.disconnect(namespace='/pty')
+
+ def tearDown(self):
+ connection = utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ utils.drop_database(connection, self.db_name)
diff --git a/web/pgadmin/tools/psql/tests/test_start_process.py b/web/pgadmin/tools/psql/tests/test_start_process.py
new file mode 100644
index 00000000..055f7ce3
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_start_process.py
@@ -0,0 +1,63 @@
+import uuid
+import config
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from regression import parent_node_dict
+from regression.test_setup import config_data
+from pgadmin.utils import server_utils as server_utils
+from pgAdmin4 import app
+from pgadmin.browser.server_groups.servers.databases.tests import utils as \
+ database_utils
+from pgadmin.utils.driver import get_driver
+from config import PG_DEFAULT_DRIVER
+from .... import socketio
+
+
+class PSQLStartProcess(BaseTestGenerator):
+ def setUp(self):
+ self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
+ database_info = parent_node_dict["database"][-1]
+ self.did = database_info["db_id"]
+ self.sid = parent_node_dict["server"][-1]["server_id"]
+ self.sgid = config_data["server_group"]
+ config.ENABLE_PSQL = True
+
+ self.server_con = server_utils.connect_server(self, self.sid)
+ print(self.server_con)
+
+ def runTest(self):
+ # Fetch flask client to access current user and other cookies.
+ flask_client = app.test_client()
+ flask_client.get('/session')
+ self.test_client = socketio.test_client(app, namespace='/pty',
+ flask_test_client=flask_client)
+ self.assertTrue(self.test_client.is_connected('/pty'))
+ received = self.test_client.get_received('/pty')
+
+ assert received[0]['name'] == 'connected'
+ assert received[0]['args'][0]['sid'] != ''
+
+ import random
+ trans_id = random.randint(1, 9999999)
+
+ data = {
+ 'sid': self.sid,
+ 'db': 'postgres',
+ 'pwd': self.server['db_password'],
+ 'user': self.server['username']
+ }
+
+ self.test_client.emit('start_process', data, namespace='/pty')
+ received = self.test_client.get_received('/pty')
+ print("received values: ", received)
+
+ self.test_client.disconnect(namespace='/pty')
+ self.assertFalse(self.test_client.is_connected('/pty'))
+
+ def tearDown(self):
+ connection = utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ utils.drop_database(connection, self.db_name)
diff --git a/web/pgadmin/tools/psql/tests/test_start_process_fail.py b/web/pgadmin/tools/psql/tests/test_start_process_fail.py
new file mode 100644
index 00000000..895fc533
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_start_process_fail.py
@@ -0,0 +1,60 @@
+import uuid
+import config
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from regression import parent_node_dict
+from regression.test_setup import config_data
+from pgadmin.utils import server_utils as server_utils
+from pgAdmin4 import app
+from pgadmin.browser.server_groups.servers.databases.tests import utils as \
+ database_utils
+from .... import socketio
+
+
+class PSQLStartProcessFail(BaseTestGenerator):
+ def setUp(self):
+ self.db_name = "psqltestdb_{0}".format(str(uuid.uuid4())[1:8])
+ self.sid = parent_node_dict["server"][-1]["server_id"]
+ self.did = utils.create_database(self.server, self.db_name)
+ self.sgid = config_data["server_group"]
+ config.ENABLE_PSQL = True
+
+ db_con = database_utils.connect_database(self,
+ self.sgid,
+ self.sid,
+ self.did)
+ print("DB connection")
+
+ def runTest(self):
+ # Fetch flask client to access current user and other cookies.
+ flask_client = app.test_client()
+ flask_client.get('/session')
+ self.test_client = socketio.test_client(app, namespace='/pty',
+ flask_test_client=flask_client)
+ self.assertTrue(self.test_client.is_connected('/pty'))
+ received = self.test_client.get_received('/pty')
+ print("received:", received)
+ assert received[0]['name'] == 'connected'
+ assert received[0]['args'][0]['sid'] != ''
+ import random
+ trans_id = random.randint(1, 9999999)
+ data = {
+ 'sid': self.sid,
+ 'db': 'postgres',
+ 'pwd': self.server['db_password'],
+ 'user': self.server['username']
+ }
+ config.ENABLE_PSQL = False
+ self.test_client.emit('start_process', data, namespace='/pty')
+ received = self.test_client.get_received('/pty')
+ assert received[0]['name'] == 'conn_not_allow'
+ self.test_client.disconnect(namespace='/pty')
+ self.assertFalse(self.test_client.is_connected('/pty'))
+
+ def tearDown(self):
+ connection = utils.get_db_connection(self.server['db'],
+ self.server['username'],
+ self.server['db_password'],
+ self.server['host'],
+ self.server['port'])
+ utils.drop_database(connection, self.db_name)
diff --git a/web/pgadmin/tools/psql/tests/utils.py b/web/pgadmin/tools/psql/tests/utils.py
new file mode 100644
index 00000000..85bd5373
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/utils.py
@@ -0,0 +1,6 @@
+import os
+import json
+
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/psql_test_data.json") as data_file:
+ test_cases = json.load(data_file)
diff --git a/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss b/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss
index 0bb40802..5ffc8241 100644
--- a/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss
+++ b/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss
@@ -374,3 +374,9 @@ div.strikeout:after {
/* Setting it to hardcoded white as the SVG generated is having white bg
* Need to check what can be done.
*/
+
+/* Css for psql */
+.psql_terminal .terminal {
+ padding-top: 1%;
+ padding-left: 0.5%;
+}
diff --git a/web/pgadmin/utils/csrf.py b/web/pgadmin/utils/csrf.py
index 23abfffa..71ae82ea 100644
--- a/web/pgadmin/utils/csrf.py
+++ b/web/pgadmin/utils/csrf.py
@@ -38,6 +38,7 @@ class _PGCSRFProtect(CSRFProtect):
'pgadmin.tools.schema_diff.ddl_compare',
'pgadmin.authenticate.login',
'pgadmin.tools.erd.panel',
+ 'pgadmin.tools.psql.panel',
]
for exempt in exempt_views:
diff --git a/web/pgadmin/utils/preferences.py b/web/pgadmin/utils/preferences.py
index 15ceeb1a..e9efe479 100644
--- a/web/pgadmin/utils/preferences.py
+++ b/web/pgadmin/utils/preferences.py
@@ -72,6 +72,7 @@ class _Preference(object):
self.select2 = kwargs.get('select2', None)
self.fields = kwargs.get('fields', None)
self.allow_blanks = kwargs.get('allow_blanks', None)
+ self.disabled = kwargs.get('disabled', False)
# Look into the configuration table to find out the id of the specific
# preference.
@@ -252,6 +253,7 @@ class _Preference(object):
'select2': self.select2,
'value': self.get(),
'fields': self.fields,
+ 'disabled': self.disabled,
}
return res
@@ -414,6 +416,7 @@ class Preferences(object):
:param fields: field schema (if preference has more than one field to
take input from user e.g. keyboardshortcut preference)
:param allow_blanks: Flag specify whether to allow blank value.
+ :param disabled: Flag specify whether to disable the setting or not.
"""
min_val = kwargs.get('min_val', None)
max_val = kwargs.get('max_val', None)
@@ -423,6 +426,7 @@ class Preferences(object):
select2 = kwargs.get('select2', None)
fields = kwargs.get('fields', None)
allow_blanks = kwargs.get('allow_blanks', None)
+ disabled = kwargs.get('disabled', False)
cat = self.__category(category, category_label)
if name in cat['preferences']:
@@ -439,7 +443,8 @@ class Preferences(object):
(cat['preferences'])[name] = res = _Preference(
cat['id'], name, label, _type, default, help_str=help_str,
min_val=min_val, max_val=max_val, options=options,
- select2=select2, fields=fields, allow_blanks=allow_blanks
+ select2=select2, fields=fields, allow_blanks=allow_blanks,
+ disabled=disabled
)
return res
diff --git a/web/webpack.config.js b/web/webpack.config.js
index e906a567..104a3b20 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -355,6 +355,7 @@ module.exports = [{
debugger_direct: './pgadmin/tools/debugger/static/js/direct.js',
schema_diff: './pgadmin/tools/schema_diff/static/js/schema_diff_hook.js',
erd_tool: './pgadmin/tools/erd/static/js/erd_tool_hook.js',
+ psql_tool: './pgadmin/tools/psql/static/js/index.js',
file_utils: './pgadmin/misc/file_manager/static/js/utility.js',
'pgadmin.style': pgadminCssStyles,
pgadmin: pgadminScssStyles,
@@ -493,7 +494,7 @@ module.exports = [{
],
},
},
- }, {
+ },{
test: require.resolve('./node_modules/acitree/js/jquery.aciTree.min'),
use: {
loader: 'imports-loader',
@@ -532,6 +533,7 @@ module.exports = [{
'pure|pgadmin.tools.storage_manager',
'pure|pgadmin.tools.search_objects',
'pure|pgadmin.tools.erd_module',
+ 'pure|pgadmin.tools.psql_module',
],
},
},
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 074b2580..e0b28091 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -159,6 +159,15 @@ var webpackShimConfig = {
'jquery.acisortable': path.join(__dirname, './node_modules/acitree/js/jquery.aciSortable.min'),
'jquery.acifragment': path.join(__dirname, './node_modules/acitree/js/jquery.aciFragment.min'),
+ //xterm
+ 'xterm': path.join(__dirname, './node_modules/xterm/lib/xterm.js'),
+ 'xterm-addon-fit': path.join(__dirname, './node_modules/xterm-addon-fit/lib/xterm-addon-fit.js'),
+ 'xterm-addon-web-links': path.join(__dirname, './node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js'),
+ 'xterm-addon-search': path.join(__dirname, './node_modules/xterm-addon-search/lib/xterm-addon-search.js'),
+
+ //socket
+ 'socketio': path.join(__dirname, './node_modules/socket.io-client/dist/socket.io.js'),
+
// Backbone and Backgrid
'backbone': path.join(__dirname, './node_modules/backbone/backbone'),
'backbone.undo': path.join(__dirname, './node_modules/backbone-undo/Backbone.Undo'),
@@ -288,6 +297,8 @@ var webpackShimConfig = {
'pgadmin.tools.storage_manager': path.join(__dirname, './pgadmin/tools/storage_manager/static/js/storage_manager'),
'pgadmin.tools.erd_module': path.join(__dirname, './pgadmin/tools/erd/static/js/erd_module'),
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
+ 'pgadmin.tools.psql_module': path.join(__dirname, './pgadmin/tools/psql/static/js/psql_module'),
+ 'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'),
'pgadmin.search_objects': path.join(__dirname, './pgadmin/tools/search_objects/static/js'),
'pgadmin.tools.user_management': path.join(__dirname, './pgadmin/tools/user_management/static/js/user_management'),
'pgadmin.user_management.current_user': '/user_management/current_user',
diff --git a/web/webpack.test.config.js b/web/webpack.test.config.js
index e0b6fd69..3cc079f9 100644
--- a/web/webpack.test.config.js
+++ b/web/webpack.test.config.js
@@ -177,6 +177,7 @@ module.exports = {
'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'),
'pgadmin.browser.activity': path.join(__dirname, './pgadmin/browser/static/js/activity'),
'pgadmin.tools.erd': path.join(__dirname, './pgadmin/tools/erd/static/js'),
+ 'pgadmin.tools.psql': path.join(__dirname, './pgadmin/tools/psql/static/js'),
'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'),
'tools': path.join(__dirname, './pgadmin/tools/'),
'pgadmin.user_management.current_user': regressionDir + '/javascript/fake_current_user',
view thread (54+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected]
Subject: Re: [pgAdmin][RM-2341]: Add menu option for starting PSQL
In-Reply-To: <CAOBg0AMVyLBma8wsbZ-VQWF4q3OAhqWeRLpmrxVcM_YOKH+MDQ@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox