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
diff --git a/docs/en_US/images/psql_tool_menu.png b/docs/en_US/images/psql_tool_menu.png
new file mode 100644
index 0000000000000000000000000000000000000000..c219a0787868bd9427cfc056ce18c9ab562e52bd
GIT binary patch
diff --git a/docs/en_US/psql_tool.rst b/docs/en_US/psql_tool.rst
new file mode 100644
index 00000000..8791648a
--- /dev/null
+++ b/docs/en_US/psql_tool.rst
@@ -0,0 +1,23 @@
+.. _psql_tool:
+
+******************
+`PSQL Tool`:index:
+******************
+
+PSQL tool allows user to connect to PostgreSQL server using psql terminal.
+
+* Open PSQL Tool from Tools menu or context menu PSQL Tool from tree view.
+
+.. image:: images/psql_tool_menu.png
+    :alt: PSQL tool menu
+    :align: center
+
+* 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..8227a4c0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,7 +8,6 @@
 #
 ###############################################################################
 
-cheroot==8.*
 Flask==1.*
 Flask-Gravatar==0.*
 Flask-Login==0.*
@@ -36,3 +35,5 @@ 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 8ed32df9..19110fda 100644
--- a/web/package.json
+++ b/web/package.json
@@ -117,13 +117,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.13.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..dc1924cb 100644
--- a/web/pgAdmin4.py
+++ b/web/pgAdmin4.py
@@ -13,7 +13,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 '
@@ -37,7 +37,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 +97,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 +208,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..fad91082 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=False, engineio_logger=False, 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..d835ca82 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,
@@ -523,3 +526,16 @@ def register_browser_preferences(self):
             '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.'
+        )
+    )
diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py
index f4165c32..01f83b11 100644
--- a/web/pgadmin/browser/server_groups/servers/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/__init__.py
@@ -34,6 +34,7 @@ from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
     SERVER_CONNECTION_CLOSED
 from sqlalchemy import or_
 from pgadmin.utils.preferences import Preferences
+from .... import socketio as sio
 
 
 def has_any(data, keys):
@@ -1499,6 +1500,13 @@ class ServerNode(PGChildNodeView):
 
         # Release Connection
         manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
+        # Check if any psql terminal is running for the current disconnecting
+        # server. If any terminate the psql tool connection.
+        if 'sid_soid_mapping' in current_app.config and sid in \
+                current_app.config['sid_soid_mapping']:
+            if sid in current_app.config['sid_soid_mapping']:
+                for i in current_app.config['sid_soid_mapping'][sid]:
+                    sio.emit('disconnect-psql', namespace='/pty', to=i)
 
         status = manager.release()
 
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..eb20cac5 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
@@ -122,6 +122,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);
       },
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 45dea090..3fb1e7d7 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/collection.js b/web/pgadmin/browser/static/js/collection.js
index 3823ef35..e763aa11 100644
--- a/web/pgadmin/browser/static/js/collection.js
+++ b/web/pgadmin/browser/static/js/collection.js
@@ -64,6 +64,14 @@ define([
               priority: 997, label: gettext('Search Objects...'),
               icon: 'fa fa-search',
             }]);
+
+            // show psql tool same as query tool.
+            pgAdmin.Browser.add_menus([{
+              name: 'show_psql_tool', node: this.type, module: this,
+              applies: ['context'], callback: 'show_psql_tool',
+              priority: 998, label: gettext('PSQL Tool (Beta)'),
+              icon: 'fas fa-terminal',
+            }]);
           }
         }
       },
@@ -498,6 +506,13 @@ define([
           pgAdmin.SearchObjects.show_search_objects('', pgAdmin.Browser.tree.selected());
         }
       },
+      show_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);
+      },
     });
 
   return pgBrowser.Collection;
diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js
index 1c5af3f5..23585dbe 100644
--- a/web/pgadmin/browser/static/js/node.js
+++ b/web/pgadmin/browser/static/js/node.js
@@ -209,6 +209,14 @@ define('pgadmin.browser.node', [
           priority: 997, label: gettext('Search Objects...'),
           icon: 'fa fa-search', enable: enable,
         }]);
+
+        // show psql tool same as query tool.
+        pgAdmin.Browser.add_menus([{
+          name: 'show_psql_tool', node: this.type, module: this,
+          applies: ['context'], callback: 'show_psql_tool',
+          priority: 998, label: gettext('PSQL Tool (Beta)'),
+          icon: 'fas fa-terminal',
+        }]);
       }
 
       // This will add options of scripts eg:'CREATE Script'
@@ -902,6 +910,15 @@ define('pgadmin.browser.node', [
         pgAdmin.DataGrid.show_query_tool('', i);
       },
 
+      // Callback to render psql tool.
+      show_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);
+      },
+
       // Logic to change the server background colour
       // There is no way of applying CSS to parent element so we have to
       // do it via JS code only
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/static/js/toolbar.js b/web/pgadmin/browser/static/js/toolbar.js
index 09baaf22..90d2d007 100644
--- a/web/pgadmin/browser/static/js/toolbar.js
+++ b/web/pgadmin/browser/static/js/toolbar.js
@@ -56,6 +56,15 @@ let _defaultToolBarButtons = [
     toggleClass: '',
     parentClass: 'pg-toolbar-btn btn-primary-icon',
     enabled: false,
+  },{
+    label: gettext('PSQL Tool'),
+    ariaLabel: gettext('PSQL Tool'),
+    btnClass: 'fas fa-terminal',
+    text: '',
+    toggled: false,
+    toggleClass: '',
+    parentClass: 'pg-toolbar-btn btn-primary-icon pg-toolbar-psql',
+    enabled: false,
   },
 ];
 
@@ -105,6 +114,13 @@ 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')){
+      var input = {},
+        t = pgAdmin.Browser.tree,
+        i = input.item || t.selected(),
+        d = i && i.length == 1 ? t.itemData(i) : undefined;
+      pgAdmin.Browser.psql.psql_tool(d, i, true);
+    }
   });
 }
 
diff --git a/web/pgadmin/browser/static/scss/_browser.scss b/web/pgadmin/browser/static/scss/_browser.scss
index 0d157384..ae72f0e6 100644
--- a/web/pgadmin/browser/static/scss/_browser.scss
+++ b/web/pgadmin/browser/static/scss/_browser.scss
@@ -53,3 +53,9 @@ samp,
   border-width: 1px;
   font-size: 1.15em;
 }
+
+.pg-toolbar-psql {
+  padding-top: 0em;
+  font-size: inherit;
+  align-items: center;
+}
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/datagrid/static/js/datagrid_panel_title.js b/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
index 1348cc45..42efc04d 100644
--- a/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
+++ b/web/pgadmin/tools/datagrid/static/js/datagrid_panel_title.js
@@ -100,6 +100,10 @@ export function generateTitle(title_placeholder, title_data) {
     title_placeholder = title_placeholder.replace(new RegExp('%ARGS%'), _.unescape(title_data.args));
     title_placeholder = title_placeholder.replace(new RegExp('%SCHEMA%'), _.unescape(title_data.schema));
     title_placeholder = title_placeholder.replace(new RegExp('%DATABASE%'), _.unescape(title_data.database));
+  } else if(title_data.type == 'psql_tool') {
+    title_placeholder = title_placeholder.replace(new RegExp('%DATABASE%'), _.unescape(title_data.database));
+    title_placeholder = title_placeholder.replace(new RegExp('%USERNAME%'), _.unescape(title_data.username));
+    title_placeholder = title_placeholder.replace(new RegExp('%SERVER%'), _.unescape(title_data.server));
   }
 
   return _.escape(title_placeholder);
diff --git a/web/pgadmin/tools/psql/__init__.py b/web/pgadmin/tools/psql/__init__.py
new file mode 100644
index 00000000..e479f627
--- /dev/null
+++ b/web/pgadmin/tools/psql/__init__.py
@@ -0,0 +1,599 @@
+#!/usr/bin/env python3
+import fcntl
+import os
+import pty
+import re
+import select
+import struct
+import termios
+import config
+import eventlet.green.subprocess as subprocess
+
+from config import PG_DEFAULT_DRIVER
+from flask import Response, url_for, request
+from flask import render_template, copy_current_request_context, \
+    current_app as app
+from flask_babelex import gettext
+from flask_security import login_required, current_user
+from pgadmin.browser.utils import underscore_unescape
+from pgadmin.utils import PgAdminModule
+from pgadmin.utils.constants import MIMETYPE_APP_JS
+from pgadmin.utils.driver import get_driver
+from ... import socketio as sio
+from pgadmin.utils import get_complete_file_path
+
+
+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')
+
+
+@blueprint.route("/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
+    )
+
+
+@blueprint.route('/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 'sid_soid_mapping' not in app.config:
+        app.config['sid_soid_mapping'] = dict()
+    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'] if params['db'] else 'postgres',
+                           server_type=params['server_type'],
+                           is_enable=config.ENABLE_PSQL,
+                           title=underscore_unescape(params['title']),
+                           theme=params['theme']
+                           )
+
+
+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)
+
+
+@sio.on('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': False}
+    else:
+        sio.emit('conn_not_allow', {'sid': request.sid}, namespace='/pty',
+                 to=request.sid)
+
+
+@sio.on('start_process', namespace='/pty')
+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'].replace('\\', "\\\\")
+
+            conn, manager = _get_connection(int(data['sid']), data)
+            psql_utility = manager.utility('sql')
+            connection_data = get_connection_str(psql_utility, db,
+                                                 manager)
+        except Exception as e:
+            # If any error raised during the start the PSQL emit error to UI.
+            # request.sid: This sid is socket id.
+            sio.emit(
+                'conn_error',
+                {
+                    'error': 'Error while running psql command: {0}'.format(e),
+                }, namespace='/pty', room=request.sid)
+
+        try:
+            if int(data['sid']) not in app.config['sid_soid_mapping']:
+                # request.sid: refer request.sid as socket id.
+                app.config['sid_soid_mapping'][data['sid']] = list()
+                app.config['sid_soid_mapping'][data['sid']].append(request.sid)
+            else:
+                app.config['sid_soid_mapping'][data['sid']].append(request.sid)
+
+            sio.start_background_task(read_and_forward_pty_output,
+                                      request.sid, data)
+        except Exception as e:
+            sio.emit(
+                'conn_error',
+                {
+                    'error': 'Error while running psql command: {0}'.format(e),
+                }, namespace='/pty', room=request.sid)
+    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()
+        # This is added for unit test only, no use in normal execution.
+        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)
+            sio.emit(sio.emit(
+                'conn_error',
+                {
+                    'error': 'Error while running psql command: {0}'
+                             ''.format('Server connection not present.'),
+                }, namespace='/pty', room=request.sid))
+            raise Exception('Server is not connected.')
+
+        return conn, manager
+    except Exception as e:
+        app.logger.error(e)
+        raise
+
+
+def get_connection_str(psql_utility, db, manager):
+    """
+    Get connection string(through connection dsn)
+    :param psql_utility: PostgreSQL binary path.
+    :param db: database name to connect specific db.
+    :return: connection attribute list for PSQL connection.
+    """
+    conn_attr = get_conn_str(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):
+    """
+    Get connection attributes for psql connection.
+    :param manager:
+    :param db:
+    :return:
+    """
+    pwd = manager.extract_password()
+    conn_attr =\
+        "host={0} port={1} dbname={2} user={3} sslmode={4} " \
+        "sslcompression={5} password={6} " \
+        "".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,
+            db if db != '' else 'postgres',
+            manager.user if manager.user else 'postgres',
+            manager.ssl_mode,
+            True if manager.sslcompression else False,
+            pwd
+        )
+
+    if 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))
+
+    if get_complete_file_path(manager.sslcert):
+        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, get_complete_file_path(manager.sslkey))
+
+    if get_complete_file_path(manager.sslrootcert):
+        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, get_complete_file_path(manager.sslcrl))
+
+    if manager.service:
+        conn_attr = " {0} service={1}".format(
+            conn_attr, get_complete_file_path(manager.service))
+
+    return conn_attr
+
+
+@sio.on('socket_input', namespace='/pty')
+def socket_input(data):
+    """
+    This get the user input through socket.
+    :param data: User input from socket.
+    """
+    try:
+        # request.sid: refer request.sid as socket id.
+        # 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:
+                    if request.sid not in session_input:
+                        session_input[request.sid] = data['input']
+                        user_input = str(session_input[request.sid]).strip()
+                    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.
+        # request.sid: refer request.sid as socket id.
+        sio.emit('pty-output',
+                 {
+                     'result': gettext('Invalid session.\r\n'),
+                     'error': True
+                 },
+                 namespace='/pty', room=request.sid)
+        del app.config['sessions'][request.sid]
+
+
+@sio.on('resize', namespace='/pty')
+def resize(data):
+    """
+    Resize the pty terminal as per the UI terminal.
+    :param data: UI terminal rows and cols data
+    """
+    # request.sid: refer request.sid as socket id.
+    if request.sid in app.config['sessions']:
+        set_term_size(app.config['sessions'][request.sid], data['rows'],
+                      data['cols'])
+
+
+@sio.on('disconnect', namespace='/pty')
+def disconnect():
+    """
+    Disconnect the socket and terminate the process
+    """
+    # request.sid: refer request.sid as socket id.
+    if request.sid in pdata:
+        # On disconnect socket manually exit the psql terminal and close the
+        # parend and child fd then kill the subprocess.
+        disconnect_socket()
+
+
+@sio.on('server-disconnect', namespace='/pty')
+def server_disconnect(data):
+    """
+    Disconnect the socket and terminate the process after user disconnect
+    the server. we can't use disconnect event name as it is reserved for socket
+    internal use.
+    """
+    # request.sid: refer request.sid as socket id.
+    if request.sid in pdata and request.sid in app.config['sid_soid_mapping'][
+            data['sid']]:
+        # On disconnect socket manually exit the psql terminal and close the
+        # parend and child fd then kill the subprocess.
+        app.config['sid_soid_mapping'][data['sid']] = [soid for soid in
+                                                       app.config[
+                                                           'sid_soid_mapping'][
+                                                           data['sid']] if
+                                                       soid != request.sid]
+        disconnect_socket()
+
+
+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]
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..6223c697
--- /dev/null
+++ b/web/pgadmin/tools/psql/static/js/psql_module.js
@@ -0,0 +1,331 @@
+/////////////////////////////////////////////////////////////
+//
+// 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 {enable} from 'pgadmin.browser.toolbar';
+import clipboard from 'sources/selection/clipboard';
+import 'wcdocker';
+import {getRandomInt} from 'sources/utils';
+
+import {getTreeNodeHierarchyFromIdentifier} from 'sources/tree/pgadmin_tree_node';
+import {generateTitle} from 'tools/datagrid/static/js/datagrid_panel_title';
+
+
+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',
+      });
+
+      var 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;
+    },
+    /* Enable/disable PSQL tool menu in tools based
+    * on node selected. if selected node is present
+    * in unsupported_nodes, menu will be disabled
+    * otherwise enabled.
+    */
+    psqlToolEnabled: function(obj) {
+
+      var isEnabled = (() => {
+        if (!_.isUndefined(obj) && !_.isNull(obj)) {
+          if (_.indexOf(pgAdmin.unsupported_nodes, obj._type) == -1 || (obj._type == 'server' && obj.connected)) {
+            if (obj._type == 'database' && obj.allowConn) {
+              return true;
+            } else if (obj._type != 'database') {
+              return true;
+            } else {
+              return false;
+            }
+          } else {
+            return false;
+          }
+        } else {
+          return false;
+        }
+      })();
+
+      enable(gettext('PSQL Tool'), isEnabled);
+      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);
+
+      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 = '';
+      // Set psql tab title as per prefrences setting.
+      var title_data = {
+        'database': parentData.database ? parentData.database.label : 'postgres' ,
+        'username': parentData.server.user_name,
+        'server': parentData.server.label,
+        'type': 'psql_tool',
+      };
+      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);
+
+      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 psql-tab-style');
+        psqlToolPanel.focus();
+
+        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);
+
+      }
+
+    },
+    getPanelUrls: function(transId, panelTitle, parentData) {
+      let openUrl = url_for('psql.panel', {
+        trans_id: transId,
+      });
+      const misc_preferences = pgBrowser.get_preferences_for_module('misc');
+      var theme = misc_preferences.theme;
+
+      openUrl += `?sgid=${parentData.server_group._id}`
+        +`&sid=${parentData.server._id}`
+        +`&server_type=${parentData.server.server_type}`
+        + `&theme=${theme}`;
+
+      if(parentData.database && parentData.database._id) {
+        let db_label = parentData.database._label.replace('\\', '\\\\');
+        openUrl += `&db=${_.escape(db_label)}`;
+      } else {
+        openUrl += `&db=${''}`;
+      }
+
+      let closeUrl = url_for('psql.close', {
+        trans_id: transId,
+      });
+      return [openUrl, closeUrl];
+    },
+    psql_terminal: function() {
+      // theme colors
+      var term = new terminal({
+        cursorBlink: true,
+        macOptionIsMeta: true,
+        scrollback: 10000,
+      });
+      return term;
+    },
+    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});
+    },
+    _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..3c550c97
--- /dev/null
+++ b/web/pgadmin/tools/psql/templates/editor_template.html
@@ -0,0 +1,161 @@
+{% 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) {
+        var theme_colors = {
+            'standard': {
+              background: '#FFF',
+              foreground: '#000',
+              cursor: '#000',
+              cursorAccent: '#000',
+              selection: '#DDD',
+            },
+            'dark': {
+              background: '#212121',
+              foreground: '#d4d4d4',
+              cursor: '#d4d4d4',
+              cursorAccent: '#d4d4d4',
+              selection: '#323e43',
+            },
+            'high_contrast': {
+              background: '#010b15',
+              foreground: '#fff',
+              cursor: '#fff',
+              cursorAccent: '#fff',
+              selection: '#84d6ff',
+            }
+        }
+        const term = self.pgAdmin.Browser.psql.psql_terminal();
+        term.setOption('theme', theme_colors['{{theme}}']);
+        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();
+                }
+            }
+
+            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 {
+                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();
+        });
+
+        socket.on("disconnect-psql", () => {
+            socket.emit("server-disconnect", {'sid': {{sid}}});
+            term.write('\r\nServer disconnected, Connection terminated,
+                TO create new connection please open another psql tool.');
+        });
+
+        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..4148c75b
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/psql_test_data.json
@@ -0,0 +1,184 @@
+{
+  "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": "First command as enter",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "input_cmd": "",
+      "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..a4f14ee4
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_backend_task.py
@@ -0,0 +1,87 @@
+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('/')
+        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..6f93c8c2
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_panel.py
@@ -0,0 +1,36 @@
+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"]
+        self.theme = 'standard'
+
+    def runTest(self):
+        trans_id = random.randint(1, 9999999)
+        url = '/psql/panel/{trans_id}?sgid={sgid}&sid={sid}&server_type=pg' \
+              '&db={db_name}&theme={theme}'.\
+            format(trans_id=trans_id, sgid=self.sgid, sid=self.sid,
+                   db_name=self.db_name, theme=self.theme)
+
+        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..7dc52498
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_psql_disabled.py
@@ -0,0 +1,34 @@
+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 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')
+
+        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..6f6eabe7
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_psql_input.py
@@ -0,0 +1,148 @@
+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('/')
+        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')
+        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')
+
+        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')
+        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..d305c2d0
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_resize_terminal.py
@@ -0,0 +1,58 @@
+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('/')
+        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')
+        self.test_client.get_received('/pty')
+
+        self.test_client.emit('resize', self.input_data, namespace='/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_socket_connect.py b/web/pgadmin/tools/psql/tests/test_socket_connect.py
new file mode 100644
index 00000000..3791525a
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_socket_connect.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 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')
+
+        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..0619eb30
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_socket_disconnect.py
@@ -0,0 +1,52 @@
+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 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):
+        # Fetch flask client to access current user and other cookies.
+        flask_test_client = app.test_client()
+        flask_test_client.get('/')
+
+        self.test_client = socketio.test_client(
+            app,
+            flask_test_client=flask_test_client,
+            namespace='/pty')
+        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')
+        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_start_process.py b/web/pgadmin/tools/psql/tests/test_start_process.py
new file mode 100644
index 00000000..1245de3f
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_start_process.py
@@ -0,0 +1,57 @@
+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 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)
+
+    def runTest(self):
+        # Fetch flask client to access current user and other cookies.
+        flask_client = app.test_client()
+        flask_client.get('/')
+        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')
+        self.test_client.get_received('/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_start_process_fail.py b/web/pgadmin/tools/psql/tests/test_start_process_fail.py
new file mode 100644
index 00000000..0acc7be2
--- /dev/null
+++ b/web/pgadmin/tools/psql/tests/test_start_process_fail.py
@@ -0,0 +1,48 @@
+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 pgAdmin4 import app
+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
+
+    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')
+
+        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']
+        }
+        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..a58eb7e9 100644
--- a/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss
+++ b/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss
@@ -374,3 +374,20 @@ 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%;
+  height: 100%;
+}
+
+.psql-icon-style {
+  font-size: inherit;
+  padding-left: 0em;
+}
+
+.psql-tab-style {
+  font-size: small;
+  padding-left: 0em;
+}
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/driver/psycopg2/server_manager.py b/web/pgadmin/utils/driver/psycopg2/server_manager.py
index 2a951b6e..3be5d833 100644
--- a/web/pgadmin/utils/driver/psycopg2/server_manager.py
+++ b/web/pgadmin/utils/driver/psycopg2/server_manager.py
@@ -527,6 +527,15 @@ WHERE db.oid = {0}""".format(did))
 
         return None
 
+    def extract_password(self):
+        if self.password:
+            crypt_key_present, crypt_key = get_crypt_key()
+            if not crypt_key_present:
+                return False, crypt_key
+
+            password = decrypt(self.password, crypt_key).decode()
+            return password
+
     def export_password_env(self, env):
         if self.password:
             crypt_key_present, crypt_key = get_crypt_key()
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 779f2faa..f631178b 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',
