pgadmin4/web/pgadmin/tools/debugger/static/js/debugger.js
2020-06-18 16:20:34 +01:00

590 lines
19 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2020, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define([
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'alertify', 'sources/pgadmin', 'pgadmin.browser',
'backbone', 'pgadmin.backgrid', 'codemirror', 'pgadmin.backform',
'pgadmin.tools.debugger.ui', 'pgadmin.tools.debugger.utils', 'sources/utils',
'wcdocker', 'pgadmin.browser.frame',
], function(
gettext, url_for, $, _, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
CodeMirror, Backform, get_function_arguments, debuggerUtils, pgadminUtils,
) {
var pgTools = pgAdmin.Tools = pgAdmin.Tools || {},
wcDocker = window.wcDocker;
/* Return back, this has been called more than once */
if (pgAdmin.Tools.Debugger)
return pgAdmin.Tools.Debugger;
pgTools.Debugger = {
init: function() {
// We do not want to initialize the module multiple times.
if (this.initialized)
return;
this.initialized = true;
// Initialize the context menu to display the debugging options when user open the context menu for functions
pgBrowser.add_menus([{
name: 'direct_debugger',
node: 'function',
module: this,
applies: ['object', 'context'],
callback: 'get_function_information',
category: gettext('Debugging'),
priority: 10,
label: gettext('Debug'),
data: {
object: 'function',
},
icon: 'fa fa-arrow-circle-right',
enable: 'can_debug',
}, {
name: 'global_debugger',
node: 'function',
module: this,
applies: ['object', 'context'],
callback: 'check_func_debuggable',
category: gettext('Debugging'),
priority: 10,
label: gettext('Set breakpoint'),
data: {
object: 'function',
debug_type: 'indirect',
},
icon: 'fa fa-arrow-circle-right',
enable: 'can_debug',
}, {
name: 'procedure_direct_debugger',
node: 'procedure',
module: this,
applies: ['object', 'context'],
callback: 'get_function_information',
category: gettext('Debugging'),
priority: 10,
label: gettext('Debug'),
data: {
object: 'procedure',
},
icon: 'fa fa-arrow-circle-right',
enable: 'can_debug',
}, {
name: 'procedure_indirect_debugger',
node: 'procedure',
module: this,
applies: ['object', 'context'],
callback: 'check_func_debuggable',
category: gettext('Debugging'),
priority: 10,
label: gettext('Set breakpoint'),
data: {
object: 'procedure',
debug_type: 'indirect',
},
icon: 'fa fa-arrow-circle-right',
enable: 'can_debug',
}, {
name: 'trigger_function_indirect_debugger',
node: 'trigger_function',
module: this,
applies: ['object', 'context'],
callback: 'check_func_debuggable',
priority: 10,
label: gettext('Set breakpoint'),
category: gettext('Debugging'),
icon: 'fa fa-arrow-circle-right',
data: {
object: 'trigger_function',
debug_type: 'indirect',
},
enable: 'can_debug',
}, {
name: 'trigger_indirect_debugger',
node: 'trigger',
module: this,
applies: ['object', 'context'],
callback: 'check_func_debuggable',
priority: 10,
label: gettext('Set breakpoint'),
category: gettext('Debugging'),
icon: 'fa fa-arrow-circle-right',
data: {
object: 'trigger',
debug_type: 'indirect',
},
enable: 'can_debug',
}, {
name: 'package_function_direct_debugger',
node: 'edbfunc',
module: this,
applies: ['object', 'context'],
callback: 'get_function_information',
category: gettext('Debugging'),
priority: 10,
label: gettext('Debug'),
data: {
object: 'edbfunc',
},
icon: 'fa fa-arrow-circle-right',
enable: 'can_debug',
}, {
name: 'package_function_global_debugger',
node: 'edbfunc',
module: this,
applies: ['object', 'context'],
callback: 'check_func_debuggable',
category: gettext('Debugging'),
priority: 10,
label: gettext('Set breakpoint'),
data: {
object: 'edbfunc',
debug_type: 'indirect',
},
icon: 'fa fa-arrow-circle-right',
enable: 'can_debug',
}, {
name: 'package_procedure_direct_debugger',
node: 'edbproc',
module: this,
applies: ['object', 'context'],
callback: 'get_function_information',
category: gettext('Debugging'),
priority: 10,
label: gettext('Debug'),
data: {
object: 'edbproc',
},
icon: 'fa fa-arrow-circle-right',
enable: 'can_debug',
}, {
name: 'package_procedure_global_debugger',
node: 'edbproc',
module: this,
applies: ['object', 'context'],
callback: 'check_func_debuggable',
category: gettext('Debugging'),
priority: 10,
label: gettext('Set breakpoint'),
data: {
object: 'edbproc',
debug_type: 'indirect',
},
icon: 'fa fa-arrow-circle-right',
enable: 'can_debug',
}]);
// Create and load the new frame required for debugger panel
this.frame = new pgBrowser.Frame({
name: 'frm_debugger',
title: gettext('Debugger'),
width: 500,
isCloseable: true,
isPrivate: true,
icon: 'fa fa-bug',
url: 'about:blank',
});
this.frame.load(pgBrowser.docker);
let self = this;
let cacheIntervalId = setInterval(function() {
if(pgBrowser.preference_version() > 0) {
self.preferences = pgBrowser.get_preferences_for_module('debugger');
clearInterval(cacheIntervalId);
}
},0);
pgBrowser.onPreferencesChange('debugger', function() {
self.preferences = pgBrowser.get_preferences_for_module('debugger');
});
},
// It will check weather the function is actually debuggable or not with pre-required condition.
can_debug: function(itemData, item, data) {
var t = 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
var tree = pgBrowser.tree,
info = tree.selected(),
d_ = info && info.length == 1 ? tree.itemData(info) : undefined,
node = d_ && pgBrowser.Nodes[d_._type];
if (!d_)
return false;
var treeInfo = node.getTreeNodeHierarchy.apply(node, [info]);
// For indirect debugging user must be super user
if (data && data.debug_type && data.debug_type == 'indirect' &&
!treeInfo.server.user.is_superuser)
return false;
// Fetch object owner
var obj_owner = treeInfo.function && treeInfo.function.funcowner ||
treeInfo.procedure && treeInfo.procedure.funcowner ||
treeInfo.edbfunc && treeInfo.edbfunc.funcowner ||
treeInfo.edbproc && 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;
}
if (d_.language != 'plpgsql' && d_.language != 'edbspl') {
return false;
}
return true;
},
/*
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: function(_url, treeInfo, node) {
var url = '{BASEURL}{URL}/{OBJTYPE}{REF}',
ref = '';
_.each(
_.sortBy(
_.values(
_.pick(treeInfo,
function(v, k) {
return (k != 'server_group');
})
),
function(o) {
return o.priority;
}
),
function(o) {
ref = pgadminUtils.sprintf('%s/%s', ref, encodeURI(o._id));
});
var 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];
});
},
check_func_debuggable: function(args, item) {
var t = pgBrowser.tree,
i = item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined,
node = d && pgBrowser.Nodes[d._type];
if (!d)
return;
var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]),
_url = this.generate_url('init', treeInfo, node);
var self = this;
$.ajax({
url: _url,
cache: false,
})
.done(function(res) {
self.start_global_debugger(args, item, res.data.trans_id);
})
.fail(function(xhr) {
try {
var err = JSON.parse(xhr.responseText);
if (err.success == 0) {
Alertify.alert(gettext('Debugger Error'), err.errormsg);
}
} catch (e) {
console.warn(e.stack || e);
}
});
},
//Callback function when user start the indirect debugging ( Listen to another session to invoke the target )
start_global_debugger: function(args, item, trans_id) {
// Initialize the target and create asynchronous connection and unique transaction ID
var t = pgBrowser.tree,
i = item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined,
node = d && pgBrowser.Nodes[d._type],
self = this;
if (!d)
return;
var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]),
baseUrl;
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': debuggerUtils.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': debuggerUtils.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,
}
);
}
$.ajax({
url: baseUrl,
method: 'GET',
})
.done(function(res) {
var url = url_for('debugger.direct', {
'trans_id': res.data.debuggerTransId,
});
if (self.preferences.debugger_new_browser_tab) {
window.open(url, '_blank');
} else {
pgBrowser.Events.once(
'pgadmin-browser:frame:urlloaded:frm_debugger',
function(frame) {
frame.openURL(url);
});
// Create the debugger panel as per the data received from user input dialog.
var dashboardPanel = pgBrowser.docker.findPanels(
'properties'
),
panel = pgBrowser.docker.addPanel(
'frm_debugger', wcDocker.DOCK.STACKED, dashboardPanel[0]
);
panel.focus();
// Panel Closed event
panel.on(wcDocker.EVENT.CLOSED, function() {
var closeUrl = url_for('debugger.close', {
'trans_id': res.data.debuggerTransId,
});
$.ajax({
url: closeUrl,
method: 'DELETE',
});
});
}
})
.fail(function(xhr) {
try {
var err = JSON.parse(xhr.responseText);
if (err.success == 0) {
Alertify.alert(gettext('Debugger Error'), err.errormsg);
}
} catch (e) {
console.warn(e.stack || e);
}
});
},
/*
Get the function information for the direct debugging to display the functions arguments and other informations
in the user input dialog
*/
get_function_information: function(args, item) {
var t = pgBrowser.tree,
i = item || t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined,
node = d && pgBrowser.Nodes[d._type],
self = this;
if (!d)
return;
var is_edb_proc = d._type == 'edbproc';
var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]),
_url = this.generate_url('init', treeInfo, node);
$.ajax({
url: _url,
cache: false,
})
.done(function(res) {
let debug_info = res.data.debug_info,
trans_id = res.data.trans_id;
// Open Alertify the dialog to take the input arguments from user if function having input arguments
if (debug_info[0]['require_input']) {
get_function_arguments(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
var t = pgBrowser.tree,
i = t.selected(),
d = i && i.length == 1 ? t.itemData(i) : undefined,
node = d && pgBrowser.Nodes[d._type];
if (!d)
return;
var treeInfo = node.getTreeNodeHierarchy.apply(node, [i]),
baseUrl;
if (d._type == 'function' || d._type == 'edbfunc') {
baseUrl = url_for(
'debugger.initialize_target_for_function', {
'debug_type': 'direct',
'trans_id': trans_id,
'sid': treeInfo.server._id,
'did': treeInfo.database._id,
'scid': treeInfo.schema._id,
'func_id': debuggerUtils.getFunctionId(treeInfo),
}
);
} else if(d._type == 'procedure' || d._type == 'edbproc') {
baseUrl = url_for(
'debugger.initialize_target_for_function', {
'debug_type': 'direct',
'trans_id': trans_id,
'sid': treeInfo.server._id,
'did': treeInfo.database._id,
'scid': treeInfo.schema._id,
'func_id': debuggerUtils.getProcedureId(treeInfo),
}
);
}
$.ajax({
url: baseUrl,
method: 'GET',
})
.done(function() {
var url = url_for('debugger.direct', {
'trans_id': trans_id,
});
if (self.preferences.debugger_new_browser_tab) {
window.open(url, '_blank');
} else {
pgBrowser.Events.once(
'pgadmin-browser:frame:urlloaded:frm_debugger',
function(frame) {
frame.openURL(url);
});
// Create the debugger panel as per the data received from user input dialog.
var dashboardPanel = pgBrowser.docker.findPanels(
'properties'
),
panel = pgBrowser.docker.addPanel(
'frm_debugger', wcDocker.DOCK.STACKED, dashboardPanel[0]
);
panel.focus();
// Register Panel Closed event
panel.on(wcDocker.EVENT.CLOSED, function() {
var closeUrl = url_for('debugger.close', {
'trans_id': trans_id,
});
$.ajax({
url: closeUrl,
method: 'DELETE',
});
});
}
})
.fail(function(e) {
Alertify.alert(
gettext('Debugger Target Initialization Error'),
e.responseJSON.errormsg
);
});
}
})
.fail(function(xhr) {
try {
var err = JSON.parse(xhr.responseText);
if (err.success == 0) {
Alertify.alert(gettext('Debugger Error'), err.errormsg);
}
} catch (e) {
console.warn(e.stack || e);
}
});
},
};
return pgAdmin.Tools.Debugger;
});