pgadmin4/web/pgadmin/tools/debugger/static/js/DebuggerModule.js

617 lines
20 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import gettext from 'sources/gettext';
import { sprintf } from 'sources/utils';
import url_for from 'sources/url_for';
import pgWindow from 'sources/window';
import Kerberos from 'pgadmin.authenticate.kerberos';
import { refresh_db_node } from 'tools/sqleditor/static/js/sqleditor_title';
import getApiInstance from '../../../../static/js/api_instance';
import { getFunctionId, getProcedureId, getAppropriateLabel, getDebuggerTitle } from './debugger_utils';
import FunctionArguments from './debugger_ui';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import DebuggerComponent from './components/DebuggerComponent';
import Theme from '../../../../static/js/Theme';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { NotifierProvider } from '../../../../static/js/helpers/Notifier';
import usePreferences, { listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import pgAdmin from 'sources/pgadmin';
import { PgAdminContext } from '../../../../static/js/BrowserComponent';
export default class DebuggerModule {
static instance;
static getInstance(...args) {
if (!DebuggerModule.instance) {
DebuggerModule.instance = new DebuggerModule(...args);
}
return DebuggerModule.instance;
}
constructor(pgAdmin, pgBrowser) {
this.pgAdmin = pgAdmin;
this.pgBrowser = pgBrowser;
this.funcArgs = new FunctionArguments();
this.api = getApiInstance();
}
init() {
if (this.initialized)
return;
this.initialized = true;
// Initialize the context menu to display the debugging options when user open the context menu for functions, procedures, triggers and trigger functions.
this.pgBrowser.add_menus([
{
name: 'direct_debugger',
node: 'function',
module: this,
applies: ['object', 'context'],
callback: 'getFunctionInformation',
category: gettext('Debugging'),
priority: 10,
label: gettext('Debug'),
data: {
object: 'function',
},
enable: 'canDebug',
}, {
name: 'global_debugger',
node: 'function',
module: this,
applies: ['object', 'context'],
callback: 'checkFuncDebuggable',
category: gettext('Debugging'),
priority: 10,
label: gettext('Set Breakpoint'),
data: {
object: 'function',
debug_type: 'indirect',
},
enable: 'canDebug',
}, {
name: 'procedure_direct_debugger',
node: 'procedure',
module: this,
applies: ['object', 'context'],
callback: 'getFunctionInformation',
category: gettext('Debugging'),
priority: 10,
label: gettext('Debug'),
data: {
object: 'procedure',
},
enable: 'can_debug',
}, {
name: 'procedure_indirect_debugger',
node: 'procedure',
module: this,
applies: ['object', 'context'],
callback: 'checkFuncDebuggable',
category: gettext('Debugging'),
priority: 10,
label: gettext('Set Breakpoint'),
data: {
object: 'procedure',
debug_type: 'indirect',
},
enable: 'can_debug',
}, {
name: 'trigger_function_indirect_debugger',
node: 'trigger_function',
module: this,
applies: ['object', 'context'],
callback: 'checkFuncDebuggable',
priority: 10,
label: gettext('Set Breakpoint'),
category: gettext('Debugging'),
data: {
object: 'trigger_function',
debug_type: 'indirect',
},
enable: 'can_debug',
}, {
name: 'trigger_indirect_debugger',
node: 'trigger',
module: this,
applies: ['object', 'context'],
callback: 'checkFuncDebuggable',
priority: 10,
label: gettext('Set Breakpoint'),
category: gettext('Debugging'),
data: {
object: 'trigger',
debug_type: 'indirect',
},
enable: 'can_debug',
}, {
name: 'package_function_direct_debugger',
node: 'edbfunc',
module: this,
applies: ['object', 'context'],
callback: 'getFunctionInformation',
category: gettext('Debugging'),
priority: 10,
label: gettext('Debug'),
data: {
object: 'edbfunc',
},
enable: 'can_debug',
}, {
name: 'package_function_global_debugger',
node: 'edbfunc',
module: this,
applies: ['object', 'context'],
callback: 'checkFuncDebuggable',
category: gettext('Debugging'),
priority: 10,
label: gettext('Set Breakpoint'),
data: {
object: 'edbfunc',
debug_type: 'indirect',
},
enable: 'can_debug',
}, {
name: 'package_procedure_direct_debugger',
node: 'edbproc',
module: this,
applies: ['object', 'context'],
callback: 'getFunctionInformation',
category: gettext('Debugging'),
priority: 10,
label: gettext('Debug'),
data: {
object: 'edbproc',
},
enable: 'can_debug',
}, {
name: 'package_procedure_global_debugger',
node: 'edbproc',
module: this,
applies: ['object', 'context'],
callback: 'checkFuncDebuggable',
category: gettext('Debugging'),
priority: 10,
label: gettext('Set Breakpoint'),
data: {
object: 'edbproc',
debug_type: 'indirect',
},
enable: 'can_debug',
}
]);
}
// It will check weather the function is actually debuggable or not with pre-required condition.
canDebug(itemData, item, data) {
let t = this.pgBrowser.tree,
i = item,
d = itemData;
// To iterate over tree to check parent node
while (i) {
if ('catalog' == d._type) {
//Check if we are not child of catalog
return false;
}
i = t.hasParent(i) ? t.parent(i) : null;
d = i ? t.itemData(i) : null;
}
// Find the function is really available in database
let tree = this.pgBrowser.tree,
info = tree.selected(),
d_ = info ? tree.itemData(info) : undefined;
if (!d_)
return false;
let treeInfo = tree.getTreeNodeHierarchy(info);
// For indirect debugging user must be super user
if (data?.debug_type == 'indirect' && !treeInfo.server.user.is_superuser)
return false;
// Fetch object owner
let obj_owner = treeInfo.function?.funcowner || treeInfo.procedure?.funcowner ||
treeInfo.edbfunc?.funcowner || treeInfo.edbproc?.funcowner;
// Must be a super user or object owner to create breakpoints of any kind
if (!(treeInfo.server.user.is_superuser || obj_owner == treeInfo.server.user.name))
return false;
// For trigger node, language will be undefined - we should allow indirect debugging for trigger node
if ((d_.language == undefined && d_._type == 'trigger') ||
(d_.language == undefined && d_._type == 'edbfunc') ||
(d_.language == undefined && d_._type == 'edbproc')) {
return true;
}
let returnValue = true;
if (d_.language != 'plpgsql' && d_.language != 'edbspl') {
returnValue = false;
}
return returnValue;
}
/*
For the direct debugging, we need to fetch the function information to display in the dialog so "generate_url"
will dynamically generate the URL from the server_id, database_id, schema_id and function id.
*/
generate_url(_url, treeInfo, node) {
let url = '{BASEURL}{URL}/{OBJTYPE}{REF}',
ref = '';
_.each(
_.sortBy(
_.values(
_.pickBy(treeInfo,
function (v, k) {
return (k != 'server_group');
})
),
function (o) {
return o.priority;
}
),
function (o) {
ref = sprintf('%s/%s', ref, encodeURI(o._id));
});
let args = {
'URL': _url,
'BASEURL': url_for('debugger.index'),
'REF': ref,
'OBJTYPE': encodeURI(node.type),
};
return url.replace(/{(\w+)}/g, function (match, arg) {
return args[arg];
});
}
getUrl(_d, newTreeInfo, trans_id) {
let baseUrl = undefined;
if (_d._type == 'function' || _d._type == 'edbfunc') {
baseUrl = url_for(
'debugger.initialize_target_for_function', {
'debug_type': 'direct',
'trans_id': trans_id,
'sid': newTreeInfo.server._id,
'did': newTreeInfo.database._id,
'scid': newTreeInfo.schema._id,
'func_id': getFunctionId(newTreeInfo),
}
);
} else if (_d._type == 'procedure' || _d._type == 'edbproc') {
baseUrl = url_for(
'debugger.initialize_target_for_function', {
'debug_type': 'direct',
'trans_id': trans_id,
'sid': newTreeInfo.server._id,
'did': newTreeInfo.database._id,
'scid': newTreeInfo.schema._id,
'func_id': getProcedureId(newTreeInfo),
}
);
}
return baseUrl;
}
checkDbNameChange(data, dbNode, newTreeInfo, db_label) {
if (data?.data_obj?.db_name != _.unescape(newTreeInfo.database.label)) {
db_label = data.data_obj.db_name;
let message = `Current database has been moved or renamed to ${db_label}. Click on the OK button to refresh the database name.`;
refresh_db_node(message, dbNode);
}
return db_label;
}
getTreeNodeData(item) {
let t = this.pgBrowser.tree,
i = item || t.selected(),
d = i ? t.itemData(i) : undefined,
node = d && this.pgBrowser.Nodes[d._type];
return [t,i,d,node];
}
/*
Get the function information for the direct debugging to display the functions arguments and other informations
in the user input dialog
*/
getFunctionInformation(args, item) {
let [t,i,d, node] = this.getTreeNodeData(item);
let self = this,
tree_data = this.pgBrowser.tree.translateTreeNodeIdFromReactTree(i),
db_data = this.pgBrowser.tree.findNode(tree_data[3]),
dbNode = db_data.domNode;
if (!d)
return;
let is_edb_proc = d._type == 'edbproc';
let treeInfo = t.getTreeNodeHierarchy(i),
_url = this.generate_url('init', treeInfo, node);
this.api({
url: _url,
method: 'GET',
}).then((res) => {
let debug_info = res.data.data.debug_info,
trans_id = res.data.data.trans_id;
// Open dialog to take the input arguments from user if function having input arguments
if (debug_info[0]['require_input']) {
self.funcArgs.show(debug_info[0], 0, is_edb_proc, trans_id);
} else {
/* Initialize the target and create asynchronous connection and unique transaction ID
If there is no arguments to the functions then we should not ask for for function arguments and
Directly open the panel */
let _t = this.pgBrowser.tree,
_i = _t.selected(),
_d = _i ? _t.itemData(_i) : undefined;
let newTreeInfo = _t.getTreeNodeHierarchy(_i);
let baseUrl = self.getUrl(_d, newTreeInfo, trans_id);
self.api({
url: baseUrl,
method: 'POST',
})
.then(function (result) {
let data = result.data.data;
let url = url_for('debugger.direct', {
'trans_id': trans_id,
});
let browser_preferences = usePreferences.getState().getPreferencesForModule('browser');
let open_new_tab = browser_preferences.new_browser_tab_open;
const db_label = self.checkDbNameChange(data, dbNode, newTreeInfo, db_label);
let label = getAppropriateLabel(newTreeInfo);
pgAdmin.Browser.Events.trigger(
'pgadmin:tool:show',
`${BROWSER_PANELS.DEBUGGER_TOOL}_${trans_id}`,
url,
null,
{title: getDebuggerTitle(browser_preferences, label, newTreeInfo.schema.label, db_label, null, self.pgBrowser),
icon: 'fa fa-bug', manualClose: false, renamable: true},
Boolean(open_new_tab?.includes('debugger'))
);
})
.catch(function (e) {
pgAdmin.Browser.notifier.alert(
gettext('Debugger Target Initialization Error'),
e.responseJSON.errormsg
);
});
}
})
.catch((err) => {
pgAdmin.Browser.notifier.alert(gettext('Debugger Error'), err.response.data.errormsg);
});
}
checkFuncDebuggable(args, item) {
let [t,i,d, node] = this.getTreeNodeData(item);
let self = this;
if (!d)
return;
let treeInfo = t.getTreeNodeHierarchy(i),
_url = this.generate_url('init', treeInfo, node);
self.api({
url: _url,
cache: false,
})
.then(function (res) {
self.startGlobalDebugger(args, item, res.data.data.trans_id);
})
.catch(function (xhr) {
self.onFail(xhr);
});
}
getGlobalUrl(d, treeInfo, trans_id) {
let baseUrl = null;
if (d._type == 'function' || d._type == 'edbfunc') {
baseUrl = url_for(
'debugger.initialize_target_for_function', {
'debug_type': 'indirect',
'trans_id': trans_id,
'sid': treeInfo.server._id,
'did': treeInfo.database._id,
'scid': treeInfo.schema._id,
'func_id': getFunctionId(treeInfo),
}
);
} else if (d._type == 'procedure' || d._type == 'edbproc') {
baseUrl = url_for(
'debugger.initialize_target_for_function', {
'debug_type': 'indirect',
'trans_id': trans_id,
'sid': treeInfo.server._id,
'did': treeInfo.database._id,
'scid': treeInfo.schema._id,
'func_id': getProcedureId(treeInfo),
}
);
} else if (d._type == 'trigger_function') {
baseUrl = url_for(
'debugger.initialize_target_for_function', {
'debug_type': 'indirect',
'trans_id': trans_id,
'sid': treeInfo.server._id,
'did': treeInfo.database._id,
'scid': treeInfo.schema._id,
'func_id': treeInfo.trigger_function._id,
}
);
} else if (d._type == 'trigger' && 'table' in treeInfo) {
baseUrl = url_for(
'debugger.initialize_target_for_trigger', {
'debug_type': 'indirect',
'trans_id': trans_id,
'sid': treeInfo.server._id,
'did': treeInfo.database._id,
'scid': treeInfo.schema._id,
'func_id': treeInfo.table._id,
'tri_id': treeInfo.trigger._id,
}
);
} else if (d._type == 'trigger' && 'view' in treeInfo) {
baseUrl = url_for(
'debugger.initialize_target_for_trigger', {
'debug_type': 'indirect',
'trans_id': trans_id,
'sid': treeInfo.server._id,
'did': treeInfo.database._id,
'scid': treeInfo.schema._id,
'func_id': treeInfo.view._id,
'tri_id': treeInfo.trigger._id,
}
);
}
return baseUrl;
}
updatedDbLabel(res, db_label, treeInfo, dbNode) {
if (res.data.data.data_obj.db_name != treeInfo.database.label) {
db_label = res.data.data.data_obj.db_name;
let message = gettext(`Current database has been moved or renamed to ${db_label}. Click on the OK button to refresh the database name.`);
refresh_db_node(message, dbNode);
}
}
//Callback function when user start the indirect debugging ( Listen to another session to invoke the target )
startGlobalDebugger(args, item, trans_id) {
// Initialize the target and create asynchronous connection and unique transaction ID
let self = this;
let t = this.pgBrowser.tree,
i = item || t.selected(),
d = i ? t.itemData(i) : undefined,
tree_data = this.pgBrowser.tree.translateTreeNodeIdFromReactTree(i),
db_data = this.pgBrowser.tree.findNode(tree_data[3]),
dbNode = db_data.domNode;
if (!d)
return;
let treeInfo = t.getTreeNodeHierarchy(i);
let baseUrl = self.getGlobalUrl(d, treeInfo, trans_id);
self.api({
url: baseUrl,
method: 'POST',
})
.then(function (res) {
let url = url_for('debugger.direct', {
'trans_id': res.data.data.debuggerTransId,
});
let browser_preferences = usePreferences.getState().getPreferencesForModule('browser');
let open_new_tab = browser_preferences.new_browser_tab_open;
const db_label = treeInfo.database.label;
self.updatedDbLabel(res, db_label, treeInfo, dbNode);
let label = getAppropriateLabel(treeInfo);
pgAdmin.Browser.Events.trigger(
'pgadmin:tool:show',
`${BROWSER_PANELS.DEBUGGER_TOOL}_${res.data.data.debuggerTransId}`,
url,
null,
{title: getDebuggerTitle(browser_preferences, label, db_label, db_label, null, self.pgBrowser),
icon: 'fa fa-bug', manualClose: false, renamable: true},
Boolean(open_new_tab?.includes('debugger'))
);
})
.catch(self.raiseError);
}
raiseError(xhr) {
try {
let err = xhr.response.data;
if (err.errormsg.search('Ticket expired') !== -1) {
let fetchTicket = Kerberos.fetch_ticket();
fetchTicket.then(
function () {
self.startGlobalDebugger();
},
function (error) {
pgAdmin.Browser.notifier.alert(gettext('Debugger Error'), error);
}
);
} else if (err.success == 0) {
pgAdmin.Browser.notifier.alert(gettext('Debugger Error'), err.errormsg);
}
} catch (e) {
console.warn(e.stack || e);
}
}
/* We should get the transaction id from the server during initialization here */
async load(container, trans_id, debug_type, function_name_with_arguments, layout) {
this.trans_id = trans_id;
this.debug_type = debug_type;
this.first_time_indirect_debug = false;
this.direct_execution_completed = false;
this.polling_timeout_idle = false;
this.debug_restarted = false;
this.is_user_aborted_debugging = false;
this.is_polling_required = true; // Flag to stop unwanted ajax calls
this.function_name_with_arguments = function_name_with_arguments;
this.layout = layout;
let selectedNodeInfo = pgWindow.pgAdmin.Browser.tree.getTreeNodeHierarchy(
pgWindow.pgAdmin.Browser.tree.selected()
);
await listenPreferenceBroadcast();
ReactDOM.render(
<Theme>
<PgAdminContext.Provider value={pgAdmin}>
<ModalProvider>
<NotifierProvider pgAdmin={pgAdmin} pgWindow={pgWindow} />
<DebuggerComponent pgAdmin={pgWindow.pgAdmin} selectedNodeInfo={selectedNodeInfo}
panelId={`${BROWSER_PANELS.DEBUGGER_TOOL}_${this.trans_id}`}
panelDocker={pgWindow.pgAdmin.Browser.docker}
layout={layout} params={{
transId: trans_id,
directDebugger: this,
funcArgsInstance: this.funcArgs
}}
/>
</ModalProvider>
</PgAdminContext.Provider>
</Theme>,
container
);
}
onFail(xhr) {
try {
let err = xhr.response.data;
if (err.success == 0) {
pgAdmin.Browser.notifier.alert(gettext('Debugger Error'), err.errormsg);
}
} catch (e) {
console.warn(e.stack || e);
}
}
}