# Copyright (c) 2009, 2020, Oracle and/or its affiliates.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2.0,
# as published by the Free Software Foundation.
#
# This program is also distributed with certain software (including
# but not limited to OpenSSL) that is licensed under separate terms, as
# designated in a particular file or component or in included license
# documentation.  The authors of MySQL hereby grant you an additional
# permission to link the program and your derivative works with the
# separately licensed software that they have included with MySQL.
# This program is distributed in the hope that it will be useful,  but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
# the GNU General Public License, version 2.0, for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

from mforms import newTreeView, newButton, newBox, newSelector, newCheckBox, newLabel, Utilities
import mforms
import grt

from workbench.db_utils import escape_sql_string, QueryError

from functools import partial

from wb_common import dprint_ex
from wb_admin_utils import weakcb, WbAdminTabBase, WbAdminValidationBase, WbAdminValidationConnection
import json

from workbench.log import log_error

class WBThreadStack(mforms.Form):
    enable_debug_info = False

    def __init__(self, ctrl_be, thread_id):
        mforms.Form.__init__(self, mforms.Form.main_form())
        self.set_title("Thread Stack for %d" % thread_id)
        self.ctrl_be = ctrl_be

        vbox = mforms.newBox(False)
        vbox.set_padding(20)
        vbox.set_spacing(18)

        self.thread_id = thread_id

        splitter = mforms.newSplitter(True, False)

        self.tree = mforms.newTreeView(mforms.TreeDefault)
        self.tree.add_column(mforms.IntegerColumnType, "Event Id", 50, False)
        self.tree.add_column(mforms.StringColumnType, "Event info", 200, False)
        self.tree.add_column(mforms.StringColumnType, "Type", 100, False)
        self.tree.add_column(mforms.StringColumnType, "Timer wait [\xC2\xB5s]", 80, False)
        if self.enable_debug_info:
            self.tree.add_column(mforms.StringColumnType, "Source", 200, False)
        self.tree.end_columns()
        self.tree.set_size(400, -1)
        self.tree.add_changed_callback(self.event_selected)
        splitter.add(self.tree, 500);

        l = mforms.newLabel("Wait info")
        l.set_style(mforms.BoldStyle)
        tbox = newBox(False)
        lbox = newBox(False)
        lbox.set_spacing(5)
        lbox.set_padding(5)
        lbox.add(l, False, False)
        tbox.add(lbox, False, False)

        self.text = mforms.newTextBox(mforms.VerticalScrollBar)
        self.text.set_read_only(True)
        self.text.set_size(150, -1)

        tbox.add(self.text, True, True)
        splitter.add(tbox, 150)

        vbox.add(splitter, True, True)

        self.set_content(vbox)

        bbox = newBox(True)
        bbox.set_spacing(8)
        self.ok = newButton()
        self.ok.set_text("Close")
        self.ok.add_clicked_callback(self.close_form)
        bbox.add_end(self.ok, False, True)
        vbox.add_end(bbox, False, True)

        self.set_size(800, 600)
        self.center()

    def close_form(self):
        self.close()

    def event_selected(self):
        node = self.tree.get_selected_node()
        if node:
            tag = node.get_tag()
            if tag:
                self.text.set_value(tag)
            else:
                self.text.clear()

    def load_data(self):
        data = None
        try:
            result = self.ctrl_be.exec_query("SELECT sys.ps_thread_stack(%d, %s)" % (self.thread_id, "TRUE" if self.enable_debug_info else "FALSE"))
            if result is not None:
                if result.nextRow():
                    data = result.stringByIndex(1)
            if data:
                data = data.replace("\0", "") # workaround for server bug
                return self.parse_data(json.loads(data))
        except Exception as e:
            import traceback
            #open("/tmp/data.js", "w+").write(data)
            log_error("Exception during sys.ps_thread_stack(%d, %s):\n%s\n" % (self.thread_id, "TRUE" if self.enable_debug_info else "FALSE", traceback.format_exc()))
            mforms.Utilities.show_error("Error Getting Thread Stack", "The thread stack for thread %d can't be loaded, please check if your sys schema is properly installed and available.\n%s" % (self.thread_id, e), "OK",  "", "")
            return None
        return False

    def parse_data(self, datatree):
        treecache = {}
        if datatree.get('events') == None or len(datatree['events']) == 0:
            return False
        for item in datatree['events']:
            node = None
            if int(item['nesting_event_id']) == 0:
                node = self.tree.add_node()
                treecache['event_%d' % int(item['event_id'])] = node
            else: #Check if there is already node, if not, we create one
                if treecache.get('event_%d' % int(item['nesting_event_id'])):
                     parent = treecache.get('event_%d' % int(item['nesting_event_id']))
                     node = parent.add_child()
                     treecache['event_%d' % int(item['event_id'])] = node
                else:
                    continue

            node.set_string(0, item['event_id'])
            node.set_string(1, item['event_info'].strip())
            node.set_string(2, item['event_type'].strip())
            if 'timer_wait' in item:
                node.set_string(3, str(item['timer_wait']))
                pnode = node.get_parent()
                if pnode:
                    pnode_twait = 0.0
                    try:
                        pnode_twait = float(pnode.get_string(3))
                    except:
                        #Parent node can be empty
                        pass
                    pnode.set_string(3, str(item['timer_wait'] + pnode_twait))

            if self.enable_debug_info:
                node.set_string(4, item['source'].strip())
            if item['wait_info'] and item['wait_info'].strip() != item['source'].strip(): 
                node.set_tag(item['wait_info'])
        #Set the textbox text as the first node
        if self.tree.count():
            tag = self.tree.node_at_row(0).get_tag()
            if tag:
                self.text.set_value(tag)
        return True

    def run(self):
        r = self.load_data()
        if not r:
            if r is not None:
                mforms.Utilities.show_error("Error Getting Thread Stack", "Thread stack is not available for thread %d. Please enable Performance Schema instrumentation (Statement and Stage instrumentations and respective History consumers)." % self.thread_id, "OK",  "", "")
            self.close()
        else:
            self.show()



class ConnectionDetailsPanel(mforms.Table):
    def __init__(self, owner):
        mforms.Table.__init__(self)

        self.owner = owner

        self.set_padding(8)
        self.set_row_spacing(4)
        self.set_column_spacing(4)
        self.set_column_count(2)
        self.set_row_count(16)

        self.labels = {}
        self.make_line("Processlist Id:", "PROCESSLIST_ID", "Process List")
        self.make_line("Thread Id:", "THREAD_ID", "Thread Identifier")
        self.make_line("Name:", "NAME", "Name")
        self.make_line("Type:", "TYPE", "Type")
        self.make_line("User:", "PROCESSLIST_USER", "Process List User")
        self.make_line("Host:", "PROCESSLIST_HOST", "Process List Host")
        self.make_line("Schema:", "PROCESSLIST_DB", "Process List Database")
        self.make_line("Command:", "PROCESSLIST_COMMAND", "Process List Command")
        self.make_line("Time:", "PROCESSLIST_TIME", "Process List Time")
        self.make_line("State:", "PROCESSLIST_STATE", "Process List State")
        self.make_line("Role:", "ROLE", "Role")
        self.make_line("Instrumented:", "INSTRUMENTED", "Instrumented")
        self.make_line("Parent Thread Id:", "PARENT_THREAD_ID", "Parent Thread Identified")

        l = mforms.newLabel("Info:")
        l.set_name("Info")
        l.set_style(mforms.BoldStyle)
        self.add(l, 0, 1, 13, 14, mforms.HFillFlag|mforms.HExpandFlag)
        self.info = mforms.newCodeEditor()
        self.info.set_features(mforms.FeatureGutter, False)
        self.info.set_features(mforms.FeatureReadOnly, True)
        self.info.set_features(mforms.FeatureWrapText, True)
        self.info.set_language(mforms.LanguageMySQL56)
        self.info.set_size(0, 50)
        self.add(self.info, 0, 2, 14, 15, mforms.HFillFlag|mforms.VFillFlag|mforms.HExpandFlag|mforms.VExpandFlag)

        if self.owner.ctrl_be.target_version.is_supported_mysql_version_at_least(5, 7, 2):
            self.explain = mforms.newButton()
            self.explain.set_enabled(False)
            self.explain.add_clicked_callback(self.owner.explain_selected)
            self.explain.set_text("Explain for Connection")
            self.add(self.explain, 1, 2, 15, 16, mforms.HFillFlag)
        else:
            self.explain = None



    def make_line(self, caption, name, accessibilityName):
        i = len(self.labels)
        l = mforms.newLabel(caption)
        l.set_name(accessibilityName)
        l.set_text_align(mforms.MiddleLeft)
        l.set_style(mforms.BoldStyle)
        self.add(l, 0, 1, i, i+1, mforms.HFillFlag|mforms.HExpandFlag)
        l = mforms.newLabel("")
        self.add(l, 1, 2, i, i+1, mforms.HFillFlag|mforms.HExpandFlag)
        self.labels[name] = l


    def update(self, node):
        self.labels["PROCESSLIST_ID"].set_text("%s" % node.get_long(0) if node else "")
        self.labels["PROCESSLIST_USER"].set_text(node.get_string(1) if node else "")
        self.labels["PROCESSLIST_HOST"].set_text(node.get_string(2) if node else "")
        self.labels["PROCESSLIST_DB"].set_text(node.get_string(3) if node else "")
        self.labels["PROCESSLIST_COMMAND"].set_text(node.get_string(4) if node else "")
        self.labels["PROCESSLIST_TIME"].set_text("%s" % node.get_long(5) if node else "")
        self.labels["PROCESSLIST_STATE"].set_text(node.get_string(6) if node else "")
        self.labels["THREAD_ID"].set_text("%s" % node.get_long(7) if node else "")
        self.labels["TYPE"].set_text(node.get_string(8) if node else "")
        self.labels["NAME"].set_text(node.get_string(9) if node else "")
        self.labels["PARENT_THREAD_ID"].set_text("%s" % node.get_long(10) if node else "")
        self.labels["INSTRUMENTED"].set_text(node.get_string(11) if node else "")

        self.info.set_features(mforms.FeatureReadOnly, False)
        self.info.set_value(node.get_tag() if node else "")
        self.info.set_features(mforms.FeatureReadOnly, True)
        if self.explain:
            self.explain.set_enabled(True if node and node.get_tag() else False)


class WbAdminValidationPermissions(WbAdminValidationBase):
    def __init__(self, ctrl_be):
        super().__init__()
        self._ctrl_be = ctrl_be
        
    def validate(self):
        try:
            self._ctrl_be.exec_query("SELECT COUNT(*) FROM performance_schema.threads")
        except QueryError as e:
            import traceback
            log_error("QueryError in Admin for Client Connections:\n%s\n\n%s\n" % (e, traceback.format_exc()))
            if e.error == 1142:
                self.set_error_message("The account you are currently using does not have sufficient privileges to view the client connections.")
            else:
                self.set_error_message("There was a problem opening the Client Connections. Please check the error log for more details.")
            return False
        except Exception as e:
            import traceback
            log_error("Exception in Admin for Client Connections:\n%s\n\n%s\n" % (e, traceback.format_exc()))
            self.set_error_message("There was a problem opening the Client Connections. Please check the error log for more details.")
            return False
        return True

class WbAdminConnections(WbAdminTabBase):
    #ui_created = False
    serial = 0

    @classmethod
    def wba_register(cls, admin_context):
        admin_context.register_page(cls, "Management", "Client Connections", "Client Connections", False)

    @classmethod
    def identifier(cls):
        return "admin_connections"

    def __init__(self, ctrl_be, instance_info, main_view):
        WbAdminTabBase.__init__(self, ctrl_be, instance_info, main_view)
        
        self.add_validation(WbAdminValidationConnection(ctrl_be))
        self.add_validation(WbAdminValidationPermissions(ctrl_be))
        
        self.set_standard_header("title_connections.png", self.instance_info.name, "Client Connections")
        
        self.set_managed()
        self.set_release_on_add()
        self._new_processlist = self.check_if_ps_available()
        self._refresh_timeout = None
        self.warning = None
        
        if self.new_processlist():
            self.columns = [("PROCESSLIST_ID", mforms.LongIntegerColumnType, "Id", 50),
                            ("PROCESSLIST_USER", mforms.StringColumnType, "User", 80),
                            ("PROCESSLIST_HOST", mforms.StringColumnType, "Host", 120),
                            ("PROCESSLIST_DB", mforms.StringColumnType, "DB", 100),
                            ("PROCESSLIST_COMMAND", mforms.StringColumnType, "Command", 80),
                            ("PROCESSLIST_TIME", mforms.LongIntegerColumnType, "Time", 60),
                            ("PROCESSLIST_STATE", mforms.StringColumnType, "State", 80),
                            ("THREAD_ID", mforms.LongIntegerColumnType, "Thread Id", 50),
                            ("TYPE", mforms.StringColumnType, "Type", 80),
                            ("NAME", mforms.StringColumnType, "Name", 80),
                            ("PARENT_THREAD_ID", mforms.LongIntegerColumnType, "Parent Thread", 50),
                            ("INSTRUMENTED", mforms.StringColumnType, "Instrumented", 80),
                            ("PROCESSLIST_INFO", mforms.StringColumnType, "Info", 200),
                            ]
            self.long_int_columns = [0, 5, 7, 10]
            self.info_column = 12
            if self.ctrl_be.target_version.is_supported_mysql_version_at_least(5, 6, 6):
                self.columns.append(("ATTR_VALUE", mforms.StringColumnType, "Program", 150))

            get_path = mforms.App.get().get_resource_path
            self.icon_for_object_type = {
                "GLOBAL" : "",
                "SCHEMA" : get_path("db.Schema.16x16.png"),
                "TABLE" : get_path("db.Table.16x16.png"),
                "FUNCTION" : get_path("db.Role.16x16.png"),
                "PROCEDURE" : get_path("db.Role.16x16.png"),
                "TRIGGER" : get_path("db.Trigger.16x16.png"),
                "EVENT" : get_path("GrtObject.16x16.png"),
                "COMMIT" : get_path("db.Column.16x16.png")
            }
        else:
            self.columns = [("Id", mforms.LongIntegerColumnType, "Id", 50),
                            ("User", mforms.StringColumnType, "User", 80),
                            ("Host", mforms.StringColumnType, "Host", 120),
                            ("DB", mforms.StringColumnType, "DB", 100),
                            ("Command", mforms.StringColumnType, "Command", 80),
                            ("Time", mforms.LongIntegerColumnType, "Time", 60),
                            ("State", mforms.StringColumnType, "State", 80),
                            ("Info", mforms.StringColumnType, "Info", 80),
                            ]
            self.long_int_columns = [0, 5]
            self.info_column = 7
            

    def create_ui(self):
        dprint_ex(4, "Enter")
        
        uiBox = mforms.newBox(False)

        if self.new_processlist():
            widths = grt.root.wb.state.get("wb.admin:ConnectionListColumnWidthsPS", None)
        else:
            widths = grt.root.wb.state.get("wb.admin:ConnectionListColumnWidths", None)
        if widths:
            column_widths = [int(i) for i in widths.split(",")]
        else:
            column_widths = None

        self.connection_box = mforms.newBox(True)
        self.connection_box.set_spacing(8)
        self.connection_list = newTreeView(mforms.TreeDefault|mforms.TreeFlatList|mforms.TreeAltRowColors)
        self.connection_list.set_selection_mode(mforms.TreeSelectMultiple)
        self.connection_list.add_column_resized_callback(self.column_resized)
        self.connection_list.set_name("Connection List")
        for i, (field, type, caption, width) in enumerate(self.columns):
            if column_widths and i < len(column_widths):
                width = column_widths[i]
            self.connection_list.add_column(type, caption, width, False)

        self.connection_list.end_columns()
        self.connection_list.set_allow_sorting(True)
        
        self.connection_list.add_changed_callback(weakcb(self, "connection_selected"))

        self.connection_box.add(self.connection_list, True, True)

        info_table = mforms.newTable()
        info_table.set_row_count(2)
        info_table.set_column_count(5)
        info_table.set_row_spacing(4)
        info_table.set_column_spacing(20)
        info_table.set_name("Counters")

        info_table.add(self.create_labeled_info("Threads Connected:", "Threads Connected", "lbl_Threads_connected"),                     0, 1, 0, 1, mforms.HFillFlag | mforms.VFillFlag)
        info_table.add(self.create_labeled_info("Threads Running:", "Threads Running", "lbl_Threads_running"),                          1, 2, 0, 1, mforms.HFillFlag | mforms.VFillFlag)
        info_table.add(self.create_labeled_info("Threads Created:", "Threads Created", "lbl_Threads_created"),                         2, 3, 0, 1, mforms.HFillFlag | mforms.VFillFlag)
        info_table.add(self.create_labeled_info("Threads Cached:", "Threads Cached", "lbl_Threads_cached"),                           3, 4, 0, 1, mforms.HFillFlag | mforms.VFillFlag)
        info_table.add(self.create_labeled_info("Rejected (over limit):", "Rejected", "lbl_Connection_errors_max_connections"), 4, 5, 0, 1, mforms.HFillFlag | mforms.VFillFlag)

        info_table.add(self.create_labeled_info("Total Connections:", "Total Connections", "lbl_Connections"),                           0, 1, 1, 2, mforms.HFillFlag | mforms.VFillFlag)
        info_table.add(self.create_labeled_info("Connection Limit:", "Connection Limit", "lbl_max_connections"),                        1, 2, 1, 2, mforms.HFillFlag | mforms.VFillFlag)
        info_table.add(self.create_labeled_info("Aborted Clients:", "Aborted Clients", "lbl_Aborted_clients"),                         2, 3, 1, 2, mforms.HFillFlag | mforms.VFillFlag)
        info_table.add(self.create_labeled_info("Aborted Connections:", "Aborted Connections", "lbl_Aborted_connects"),                    3, 4, 1, 2, mforms.HFillFlag | mforms.VFillFlag)
        info_table.add(self.create_labeled_info("Errors:", "Errors", "lbl_errors", "tooltip_errors"),                         4, 5, 1, 2, mforms.HFillFlag | mforms.VFillFlag)

        self.info_table = info_table
        uiBox.add(info_table, False, True)

        uiBox.add(self.connection_box, True, True)

        box = newBox(True)
        self.button_box = box
        self.button_box.set_spacing(12)
        
        refresh_button = newButton()
        refresh_button.set_text("Refresh")
        self.button_box.add_end(refresh_button, False, True)
        refresh_button.add_clicked_callback(weakcb(self, "refresh"))

        self.kill_button = newButton()
        self.kill_button.set_text("Kill Connection(s)")
        self.kill_button.set_name("Kill Connections")
        self.button_box.add_end(self.kill_button, False, True)
        self.kill_button.add_clicked_callback(weakcb(self, "kill_connection"))
        
        self.killq_button = newButton()
        self.killq_button.set_text("Kill Query(s)")
        self.killq_button.set_name("Kill Queries")
        self.button_box.add_end(self.killq_button, False, True)
        self.killq_button.add_clicked_callback(weakcb(self, "kill_query"))
        
        refresh_label = newLabel("Refresh Rate:")
        self.button_box.add(refresh_label, False, True)

        self._menu = mforms.newContextMenu()
        self._menu.add_will_show_callback(self.menu_will_show)
        self.connection_list.set_context_menu(self._menu)

        
        self.refresh_values = [0.5, 1, 2, 3, 4, 5, 10, 15, 30]
        self.refresh_values_size = len(self.refresh_values)
        
        self.refresh_selector = newSelector()
        self.refresh_selector.set_size(100,-1)
        
        for s in self.refresh_values:
            self.refresh_selector.add_item(str(s) + " seconds")
        
        self.refresh_selector.add_item("Don't Refresh")
        
        refresh_rate_index = grt.root.wb.options.options.get('Administrator:refresh_connections_rate_index', 9)
        self.refresh_selector.set_selected(refresh_rate_index)
        self.update_refresh_rate()
        self.refresh_selector.add_changed_callback(weakcb(self, "update_refresh_rate"))
        self.button_box.add(self.refresh_selector, False, True)
        

        self.check_box = newBox(True)
        self.check_box.set_spacing(12)

        self.hide_sleep_connections = newCheckBox()
        self.hide_sleep_connections.set_text('Hide sleeping connections')
        self.hide_sleep_connections.set_name('Hide sleeping connections')
        self.hide_sleep_connections.add_clicked_callback(self.refresh)
        self.hide_sleep_connections.set_tooltip('Remove connections in the Sleeping state from the connection list.')
        self.check_box.add(self.hide_sleep_connections, False, True)

        self.mdl_locks_page = None
        self._showing_extras = False
        if self.new_processlist():
            self.hide_background_threads = newCheckBox()
            self.hide_background_threads.set_active(True)
            self.hide_background_threads.set_text('Hide background threads')
            self.hide_background_threads.set_name('Hide background threads')
            self.hide_background_threads.set_tooltip('Remove background threads (internal server threads) from the connection list.')
            self.hide_background_threads.add_clicked_callback(self.refresh)
            self.check_box.add(self.hide_background_threads, False, True)
            
            self.truncate_info = newCheckBox()
            self.truncate_info.set_active(True)
            self.truncate_info.set_text('Don\'t load full thread info')
            self.truncate_info.set_name('Do Not Load Thread Info')
            self.truncate_info.set_tooltip('Toggle whether to load the entire query information for all connections or just the first 255 characters.\nEnabling this can have a large impact in busy servers or server executing large INSERTs.')
            self.truncate_info.add_clicked_callback(self.refresh)
            self.check_box.add(self.truncate_info, False, True)

            # tab with some extra info, only available if PS exists
            self.extra_info_tab = mforms.newTabView(mforms.TabViewSystemStandard)
            self.extra_info_tab.set_size(350, -1)
            self.extra_info_tab.add_tab_changed_callback(self.extra_tab_changed)
            self.extra_info_tab.set_name("Details Sidebar")

            self.connection_details_scrollarea = mforms.newScrollPanel()
            self.connection_details = ConnectionDetailsPanel(self)
            self.connection_details_scrollarea.add(self.connection_details)
            self.details_page = self.extra_info_tab.add_page(self.connection_details_scrollarea, "Details")

            self.mdl_list_box = None
            if self.ctrl_be.target_version.is_supported_mysql_version_at_least(5, 7, 3):
                self.mdl_list_box_scrollarea = mforms.newScrollPanel()
                self.mdl_list_box = mforms.newBox(False)
                self.mdl_list_box_scrollarea.add(self.mdl_list_box)

                self.mdl_label = mforms.newLabel('Metadata locks (MDL) protect concurrent access to\nobject metadata (not table row/data locks)')
                self.mdl_label.set_name("Metadata Info")
                self.mdl_list_box.add(self.mdl_label, False, True)

                label = mforms.newLabel("\nGranted Locks (and threads waiting on them)")
                label.set_name("Granted Locks Info")
                label.set_style(mforms.BoldStyle)
                self.mdl_list_box.add(label, False, True)
                label = mforms.newLabel("Locks this connection currently owns and\nconnections that are waiting for them.")
                label.set_name("Lock Connection Info")
                label.set_style(mforms.SmallHelpTextStyle)
                self.mdl_list_box.add(label, False, True)

                self.mdl_list_held = mforms.newTreeView(mforms.TreeAltRowColors)
                self.mdl_list_held.add_column(mforms.IconStringColumnType, "Object", 130, False)
                self.mdl_list_held.add_column(mforms.StringColumnType, "Type", 100, False)
                self.mdl_list_held.add_column(mforms.StringColumnType, "Duration", 100, False)
                self.mdl_list_held.end_columns()
                self.mdl_list_held.set_size(0, 100)
                self.mdl_list_box.add(self.mdl_list_held, True, True)

                label = mforms.newLabel("\nPending Locks")
                label.set_style(mforms.BoldStyle)
                self.mdl_list_box.add(label, False, True)
                hbox = mforms.newBox(True)
                hbox.set_spacing(4)
                self.mdl_blocked_icon = mforms.newImageBox()
                self.mdl_blocked_icon.set_image(mforms.App.get().get_resource_path("message_warning.png"))
                hbox.add(self.mdl_blocked_icon, False, True)
                self.mdl_waiting_label = mforms.newLabel("Locks this connection is currently waiting for.")
                self.mdl_waiting_label.set_name("Lock Connection Info")
                hbox.add(self.mdl_waiting_label, True, True)
                self.mdl_list_box.add(hbox, False, True)
                self.mdl_locks_page = self.extra_info_tab.add_page(self.mdl_list_box_scrollarea, "Locks")

            if self.ctrl_be.target_version.is_supported_mysql_version_at_least(5, 6, 0):
                self.attributes_list = mforms.newTreeView(mforms.TreeFlatList|mforms.TreeAltRowColors)
                self.attributes_list.add_column(mforms.StringColumnType, "Attribute", 150, False)
                self.attributes_list.add_column(mforms.StringColumnType, "Value", 200, False)
                self.attributes_list.end_columns()
                self.attributes_page = self.extra_info_tab.add_page(self.attributes_list, "Attributes")

            self.connection_box.add(self.extra_info_tab, False, True)
            self.extra_info_tab.show(False)

            self.show_extras = newButton()
            self.show_extras.set_text('Show Details')
            self.show_extras.add_clicked_callback(self.toggle_extras)
            self.check_box.add_end(self.show_extras, False, True)
            self.check_box.set_padding(0, 5, 0, 5)
            
        
        footerBox = mforms.newBox(False)
        footerBox.add_end(self.check_box, False, True)
        footerBox.add_end(self.button_box, False, True)
        
        self.set_footer(footerBox)
        
        self.resume_layout()
        
        self.connection_selected()
        
        self.relayout()
        
        dprint_ex(4, "Leave")
        
        # Call refresh to load list at least once
        self.refresh()

        return uiBox


    def shutdown(self):
        if self._refresh_timeout:
            Utilities.cancel_timeout(self._refresh_timeout)
            self._refresh_timeout = None

        
    def create_labeled_info(self, lbl_txt, acc_name, lbl_name, tooltip_name = None):
        lbox = newBox(True)
        lbox.set_spacing(5)
        l = mforms.newLabel(lbl_txt)
        l.set_name(acc_name)
        lbox.add(l, False, True)
        setattr(self, lbl_name, mforms.newLabel(""))
        l.set_style(mforms.BoldStyle)
        lbox.add(getattr(self, lbl_name), False, True)
        if tooltip_name != None:
            i = mforms.newImageBox()
            i.set_image(mforms.App.get().get_resource_path("mini_notice.png"))
            i.set_tooltip("")
            i.set_name(tooltip_name)
            lbox.add(i, False, True)
            setattr(self, tooltip_name, i)

        return lbox

    def load_info_panel_data(self):
        vars = ['Threads_connected', 'Threads_running', 'Threads_created', 'Threads_cached', 'Connections', 'Aborted_clients', 'Aborted_connects']
        errors = []
        if self.ctrl_be.target_version.is_supported_mysql_version_at_least(5, 6, 5):
            vars.append('Connection_errors_max_connections')
            errors = ['Connection_errors_accept', 'Connection_errors_internal', 'Connection_mac_connections', 'Connection_per_addr', 'Connection_errors_select', 'Connection_errors_tcpwrap']
        error_tooltip = []
        error_count = 0
        result = self.ctrl_be.exec_query("SHOW GLOBAL STATUS")
        if result is not None:
            while result.nextRow():
                if result.stringByIndex(1) in vars:
                    obj = getattr(self, "lbl_%s" % result.stringByIndex(1), None)
                    if obj != None:
                        obj.set_text(result.stringByIndex(2))
                if result.stringByIndex(1) in errors:
                    error_count += int(result.stringByIndex(2))
                    error_tooltip.append("%s: %d" % (result.stringByIndex(1), int(result.stringByIndex(2))))
        
        if error_tooltip:
            self.lbl_errors.show(True)
            self.tooltip_errors.set_tooltip("\n".join(error_tooltip))
            self.lbl_errors.set_text("%d" % error_count)
        else:
            self.lbl_errors.show(False)
        
        self.lbl_max_connections.set_text(self.ctrl_be.server_variables['max_connections'])

    def menu_will_show(self, item):
        if item is None:
            self._menu.remove_all()
            
            selected_conn = self.connection_list.get_selection()

            if selected_conn:
                user_thread = self.new_processlist() == False
                if not user_thread:
                    user_thread = len([sel for sel in selected_conn if not sel.get_string(8).startswith('BACKGROUND')]) > 0

                item = self._menu.add_item_with_title("Copy", self.copy_selected, "Copy Selected", "copy_selected")
                item = self._menu.add_item_with_title("Copy Info", self.copy_selected_info, "Copy Selected Info", "copy_selected_info")
                item = self._menu.add_item_with_title("Show in Editor", self.edit_selected, "Edit Selected", "edit_selected")
                item.set_enabled(user_thread)

                if self.ctrl_be.target_version.is_supported_mysql_version_at_least(5, 7):
                    self._menu.add_separator()
                    item = self._menu.add_item_with_title("Explain for Connection", self.explain_selected, "Explain", "explain")
                    if len(selected_conn) != 1:
                        item.set_enabled(False)
                    else:
                        info = selected_conn[0].get_string(self.info_column)
                        if not info or info == "NULL":
                            item.set_enabled(False)

                if self.new_processlist():
                    item = self._menu.add_item_with_title("View Thread Stack", self.view_thread_stack, "View Thread Stack", "view_thread_stack")
                    if len(selected_conn) != 1:
                        item.set_enabled(False)
                    instr_caption = "Enable Instrumentation for Thread"
                    start_with_yes =  [sel.get_string(11).startswith('YES') for sel in selected_conn]

                    if all(start_with_yes):
                        instr_caption = "Disable Instrumentation for Thread"
                    item = self._menu.add_item_with_title(instr_caption, self.enable_disable_instrumentation, "Enable Instrumentation", "enable_disable_instrumentation")
                    if any(start_with_yes) and not all(start_with_yes):
                        item.set_enabled(False)                    

                self._menu.add_separator()
                item = self._menu.add_item_with_title("Kill Query(s)", self.kill_query, "Kill Query", "kill_query")
                item.set_enabled(user_thread)
                item = self._menu.add_item_with_title("Kill Connection(s)", self.kill_connection, "Kill Connection", "kill_connection")
                item.set_enabled(user_thread)
                self._menu.add_separator()
            self._menu.add_item_with_title("Refresh", self.refresh, "Refresh", "refresh")

    def column_resized(self, col):
        widths = []
        for c in range(self.connection_list.get_column_count()):
            widths.append(self.connection_list.get_column_width(c))
        widths = ",".join([str(w) for w in widths])
        if self.new_processlist():
            grt.root.wb.state["wb.admin:ConnectionListColumnWidthsPS"] = widths
        else:
            grt.root.wb.state["wb.admin:ConnectionListColumnWidths"] = widths


    def check_if_ps_available(self):
        if self.ctrl_be.target_version and self.ctrl_be.target_version.is_supported_mysql_version_at_least(5, 6):
            result = self.ctrl_be.exec_query("select @@performance_schema")
            if result:
                if result.nextRow():
                    if result.intByIndex(1) == 1:
                        return True
        return False


    def check_if_mdl_available(self):
        if self.ctrl_be.target_version and self.ctrl_be.target_version.is_supported_mysql_version_at_least(5, 7, 3):
            result = self.ctrl_be.exec_query("select count(*) from performance_schema.setup_instruments where name = 'wait/lock/metadata/sql/mdl' and enabled='YES'")
            if result:
                if result.nextRow():
                    if result.intByIndex(1) == 1:
                        return True
        return False


    def new_processlist(self):
        return self._new_processlist
    
    def connection_selected(self):
        dprint_ex(4, "Enter")
        sel = self.connection_list.get_selection()
        if not sel:
            self.kill_button.set_enabled(False)
            self.killq_button.set_enabled(False)
        else:
            self.kill_button.set_enabled(True)
            self.killq_button.set_enabled(True)

        if self._showing_extras:
            if sel and len(sel) == 1:
                self.connection_details.update(sel[0])
            else:
                self.connection_details.update(None)
            tab = self.extra_info_tab.get_active_tab()
            if tab == self.mdl_locks_page:
                self.refresh_mdl_list()
            elif tab == self.attributes_page:
                self.refresh_attr_list()

        dprint_ex(4, "Leave")


    def refresh_attr_list(self):
        self.attributes_list.clear()

        try:
            nodes = self.connection_list.get_selection()
            if nodes and len(nodes) == 1:
                connid = nodes[0].get_long(0)

                result = self.ctrl_be.exec_query("SELECT * FROM performance_schema.session_connect_attrs WHERE processlist_id = %s ORDER BY ORDINAL_POSITION" % connid)
                while result and result.nextRow():
                    node = self.attributes_list.add_node()
                    node.set_string(0, result.stringByName("ATTR_NAME"))
                    node.set_string(1, result.stringByName("ATTR_VALUE"))
        except Exception as e:
            import traceback
            log_error("Error looking up attribute information: %s\n" % traceback.format_exc())
            mforms.Utilities.show_error("Lookup Connection Attributes", "Error looking up connection attributes: %s" % e, "OK", "", "")


    def refresh_mdl_list(self):
        self.mdl_list_held.clear()

        self.mdl_blocked_icon.show(False)
        waiting_label_text = "This connection is not waiting for any locks."

        try:
            nodes = self.connection_list.get_selection()
            if nodes and len(nodes) == 1:
                thread_id = nodes[0].get_long(7)

                result = self.ctrl_be.exec_query("SELECT * FROM performance_schema.metadata_locks WHERE owner_thread_id = %s" % thread_id)
                if result is not None:
                    while result.nextRow():
                        lock_status = result.stringByName("LOCK_STATUS")
                        if lock_status == "PENDING":
                            otype = result.stringByName("OBJECT_TYPE")
                            oschema = result.stringByName("OBJECT_SCHEMA")
                            oname = result.stringByName("OBJECT_NAME")
                            obj_name = [oschema, oname]
                            if otype == "GLOBAL":
                                obj_name = "<global>"
                            else:
                                obj_name =  ".".join([o for o in obj_name if o is not None])
                            self.mdl_blocked_icon.show(True)

                            sub_expr = "OBJECT_TYPE = '%s'" % otype
                            if oschema:
                                sub_expr += " AND OBJECT_SCHEMA = '%s'" % escape_sql_string(oschema)
                            if oname:
                                sub_expr += " AND OBJECT_NAME = '%s'" % escape_sql_string(oname)

                            lock_type = result.stringByName("LOCK_TYPE")
                            lock_duration = result.stringByName("LOCK_DURATION")

                            subresult = self.ctrl_be.exec_query("""SELECT *
                                FROM performance_schema.metadata_locks
                                WHERE %s AND LOCK_STATUS = 'GRANTED'""" % sub_expr)
                            owners = []
                            while subresult and subresult.nextRow():
                                owners.append(subresult.intByName("OWNER_THREAD_ID"))
                            owner_list = ", ".join([str(i) for i in owners])
                            if len(owners) == 1:
                                waiting_label_text = "The connection is waiting for a lock on\n%s %s,\nheld by thread %s." % (otype.lower(), obj_name, owner_list)
                            else:
                                waiting_label_text = "The connection is waiting for a lock on\n%s %s,\nheld by threads %s" % (otype.lower(), obj_name, owner_list)
                            waiting_label_text += "\nType: %s\nDuration: %s" % (lock_type, lock_duration)
                        elif lock_status == "GRANTED":
                            node = self.mdl_list_held.add_node()

                            otype = result.stringByName("OBJECT_TYPE")
                            oschema = result.stringByName("OBJECT_SCHEMA")
                            oname = result.stringByName("OBJECT_NAME")
                            obj_name = [oschema, oname]
                            if otype == "GLOBAL":
                                node.set_string(0, "<global>")
                            else:
                                node.set_string(0, ".".join([o for o in obj_name if o is not None]))
                            node.set_icon_path(0, self.icon_for_object_type.get(otype))

                            sub_expr = "OBJECT_TYPE = '%s'" % otype
                            if oschema:
                                sub_expr += " AND OBJECT_SCHEMA = '%s'" % escape_sql_string(oschema)
                            if oname:
                                sub_expr += " AND OBJECT_NAME = '%s'" % escape_sql_string(oname)

                            node.set_string(1, result.stringByName("LOCK_TYPE"))
                            node.set_string(2, result.stringByName("LOCK_DURATION"))

                            subresult = self.ctrl_be.exec_query("""SELECT OWNER_THREAD_ID, LOCK_TYPE, LOCK_DURATION
                                        FROM performance_schema.metadata_locks
                                        WHERE %s AND LOCK_STATUS = 'PENDING'""" % sub_expr)
                            while subresult and subresult.nextRow():
                                subnode = node.add_child()
                                subnode.set_string(0, "thread %s" % subresult.intByName("OWNER_THREAD_ID"))
                                subnode.set_string(1, subresult.stringByName("LOCK_TYPE"))
                                subnode.set_string(2, subresult.stringByName("LOCK_DURATION"))
            else:
                waiting_label_text = ""
        except Exception as e:
            import traceback
            log_error("Error looking up metadata lock information: %s\n" % traceback.format_exc())
            mforms.Utilities.show_error("Lookup Metadata Locks", "Error looking up metadata lock information: %s" % e, "OK", "", "")

        self.mdl_waiting_label.set_text(waiting_label_text)

    def get_process_list(self):
        if self.new_processlist():
            return self.get_process_list_new()
        else:
            return self.get_process_list_old()
        
    def get_process_list_new(self):
        cols = []
        for field, type, caption, width in self.columns:
            if field == "PROCESSLIST_USER":
                cols.append("IF (NAME = 'thread/sql/event_scheduler','event_scheduler',t.PROCESSLIST_USER) PROCESSLIST_USER")
            elif field == "INFO" and self.truncate_info.get_active():
                cols.append("SUBSTR(t.INFO, 0, 255) INFO")
            else:
                if field == "ATTR_VALUE":
                    cols.append("a."+field)
                else:
                    cols.append("t."+field)

        if self.ctrl_be.target_version.is_supported_mysql_version_at_least(5, 6, 6):
            JOIN = " LEFT OUTER JOIN performance_schema.session_connect_attrs a ON t.processlist_id = a.processlist_id AND (a.attr_name IS NULL OR a.attr_name = 'program_name')"
        else:
            JOIN = ""
        if self.hide_background_threads.get_active():
            result = self.ctrl_be.exec_query("SELECT %s FROM performance_schema.threads t %s WHERE t.TYPE <> 'BACKGROUND'" % (",".join(cols), JOIN))
        else:
            result = self.ctrl_be.exec_query("SELECT %s FROM performance_schema.threads t %s WHERE 1=1" % (",".join(cols), JOIN))
        
        if result is not None:
            result_rows = []
            while result.nextRow():
                row = []
                for i, (field, type, caption, width) in enumerate(self.columns):
                    value = result.stringByName(field)
                    row.append(value)
                result_rows.append(row)
            return result_rows
        return None
    
    def get_process_list_old(self):
        result = self.ctrl_be.exec_query("SHOW FULL PROCESSLIST")
        if result is not None:
            result_rows = []
            while result.nextRow():
                row = []
                for field, type, caption, width in self.columns:
                    value = result.stringByName(field)
                    row.append(value)
                result_rows.append(row)
            return result_rows
        
        return None
    
    
    def update_refresh_rate(self):
        index = int(self.refresh_selector.get_selected_index())
        grt.root.wb.options.options['Administrator:refresh_connections_rate_index'] = index
        self.serial += 1
        if self._refresh_timeout:
            Utilities.cancel_timeout(self._refresh_timeout)
            self._refresh_timeout = None
        if (index < self.refresh_values_size):
            self._refresh_timeout = Utilities.add_timeout(self.refresh_values[index], partial(self.refresh, my_serial = self.serial))

    def enable_disable_instrumentation(self):
        selected_conn = self.connection_list.get_selection()
        if not selected_conn:
            return
        instr_state = 'YES'
        if selected_conn[0].get_string(11).startswith('YES'):
            instr_state = 'No'

        for sel in selected_conn:
            connid = sel.get_long(7)
            try:
                self.ctrl_be.exec_sql("UPDATE performance_schema.threads SET instrumented = '%s' WHERE thread_id = %d LIMIT 1" % (instr_state, connid))
            except Exception as e:
                log_error("Error enabling thread instrumentation: %s\n" % e)
                mforms.Utilities.show_error("Toggle Thread Instrumentation", "Error setting instrumentation for thread %d: %s" % (connid, e), "OK",  "", "")
                break

        self.refresh()


    def extra_tab_changed(self):
        self.connection_selected()


    def enable_mdl_instrumentation(self):
        try:
            self.ctrl_be.exec_sql("UPDATE performance_schema.setup_instruments SET enabled='YES' WHERE name = 'wait/lock/metadata/sql/mdl'")
        except Exception as e:
            log_error("Error enabling MDL instrumentation: %s\n" % e)
            mforms.Utilities.show_error("Enable MDL Instrumentation", "Error enabling performance_schema MDL instrumentation.\n%s" % e, "OK",  "", "")
            return

        self._mdl_enabled = self.check_if_mdl_available()
        if self._mdl_enabled:
            self.mdl_list_box.remove(self.mdl_enable_button_sep)
            self.mdl_list_box.remove(self.mdl_enable_button)
            self.mdl_enable_button_sep = None
            self.mdl_enable_button = None
        else:
            log_error("MDL instrumentation enabled, but it's still disabled!?\n")


    _mdl_enabled = None
    def toggle_extras(self):
        self._showing_extras = not self._showing_extras
        if self._showing_extras:
            if self._mdl_enabled is None:
                self._mdl_enabled = self.check_if_mdl_available()

            if not self._mdl_enabled and self.mdl_list_box:
                self.mdl_enable_button_sep = mforms.newLabel("\n\nMDL instrumentation is currently disabled.\nClick [Enable Instrumentation] to enable it.")
                self.mdl_list_box.add(self.mdl_enable_button_sep, False, True)

                self.mdl_enable_button = mforms.newButton()
                self.mdl_enable_button.add_clicked_callback(self.enable_mdl_instrumentation)
                self.mdl_enable_button.set_text("Enable Instrumentation")
                self.mdl_list_box.add(self.mdl_enable_button, False, True)

            self.extra_info_tab.show(True)
            self.connection_selected()
            self.show_extras.set_text("Hide Details")
        else:
            self.extra_info_tab.show(False)
            self.show_extras.set_text("Show Details")


    def view_thread_stack(self):
        sel = self.connection_list.get_selected_node()
        if not sel:
            return

        view = WBThreadStack(self.ctrl_be, sel.get_long(7))
        view.run()

    def copy_selected_info(self):
        selected_conn = self.connection_list.get_selection()
        if not selected_conn:
            return

        info = ', '.join([sel.get_tag() for sel in selected_conn if sel.get_tag()])
        if len(info) > 250 and self.new_processlist() and self.truncate_info.get_active():
            info += " /* statement may be truncated */"
        mforms.Utilities.set_clipboard_text(info)


    def _node_text(self, sel):
        text = []
        text.append("-- Connection Id: %s\n" % sel.get_long(0))
        text.append("-- User: %s\n" % sel.get_string(1))
        text.append("-- Host: %s\n" % sel.get_string(2))
        text.append("-- DB: %s\n" % sel.get_string(3))
        text.append("-- Command: %s\n" % sel.get_string(4))
        text.append("-- Time: %s\n" %  sel.get_long(5))
        text.append("-- State: %s\n" % sel.get_string(6))
        info = sel.get_tag()
        if len(info) > 250 and self.new_processlist() and self.truncate_info.get_active():
            info += " /* statement may be truncated */"
        text.append(info)
        return "".join(text)


    def copy_selected(self):
        selected_conn = self.connection_list.get_selection()
        if not selected_conn:
            return
        mforms.Utilities.set_clipboard_text('\n'.join([self._node_text(sel) for sel in selected_conn]))


    def edit_selected(self):
        selected_conn = self.connection_list.get_selection()
        if not selected_conn:
            return
        editor = self.main_view.editor.addQueryEditor()
        editor.replaceContents('\n'.join([self._node_text(sel) for sel in selected_conn]))
        return editor

    def explain_selected(self):
        sel = self.connection_list.get_selected_node()
        if not sel:
            return

        editor = self.edit_selected()
        grt.modules.SQLIDEQueryAnalysis.visualExplainForConnection(editor, str(sel.get_long(0)), sel.get_string(self.info_column))


    def refresh(self, query_result = None, my_serial = 0):
        if not self.page_active():
            dprint_ex(2, "Leave. Page is inactive")
            return True
 
        if not self.ctrl_be.is_sql_connected():
            dprint_ex(2, "Leave. SQL connection is offline")
            self._refresh_timeout = None
            return False
       
        self.load_info_panel_data()

        if self.new_processlist():
            id_column = 7
        else:
            id_column = 0

        node = self.connection_list.get_selected_node()
        if node:
            old_selected = node.get_long(id_column)
        else:
            old_selected = None
        old_selected_node = None
        
        if query_result is None:
            query_result = self.get_process_list()
        
        if query_result is not None:
            self.connection_list.freeze_refresh()
            self.connection_list.clear()
            no_sleep_connections = self.hide_sleep_connections.get_active()
            
            no_bg_threads = False
            if self.new_processlist():
                no_bg_threads = self.hide_background_threads.get_active()
            try:
                for row in query_result:
                    if no_sleep_connections and str(row[4]).startswith('Sleep'):
                        continue
                    if no_bg_threads and str(row[8]).startswith('BACKGROUND'):
                         continue
                    r = self.connection_list.add_node()
                    for c, field in enumerate(row):
                        if c in self.long_int_columns:
                            try:
                                field = int(field)
                            except Exception:
                                field = 0
                            r.set_long(c, field)
                            if field == old_selected and id_column == c:
                              old_selected_node = r
                        elif c == self.info_column:
                            # truncate Info column to 255 chars for display, since they can be REALLY long
                            # which causes GDI trouble in Windows... so just store the full info in the tag
                            if field is not None:
                                r.set_string(c, field[:255])
                            else:
                                r.set_string(c, "NULL")
                            r.set_tag(field or "")
                        else:
                            field = str(field)
                            r.set_string(c, field)
            
            finally:
                self.connection_list.thaw_refresh()
            
            if old_selected_node:
                self.connection_list.select_node(old_selected_node)
            
            self.connection_selected()
        
        cont = (my_serial == self.serial)
        if not cont:
            self._refresh_timeout = None
        return cont
    
    
    def kill_connection(self):
        if not self.ctrl_be.is_sql_connected():
            return
          
        selections = self.connection_list.get_selection()
        if not selections:
            return
        
        for sel in selections:
            connid = sel.get_long(0)
            if self.new_processlist() and sel.get_string(8).startswith('BACKGROUND'):
                mforms.Utilities.show_error("Error Killing Connection", "Thread %s cannot be killed" % connid, "OK",  "", "")
                return

            try:
                self.ctrl_be.exec_sql("KILL CONNECTION %s"%connid)
            except Exception as e:
                mforms.Utilities.show_error("Error Killing Connection", "%s" % e, "OK",  "", "")
                break
                
        self.refresh()
    
    
    def kill_query(self):
        if not self.ctrl_be.is_sql_connected():
            return
        
        selections = self.connection_list.get_selection()
        if not selections:
            return
        
        for sel in selections:
            connid = sel.get_long(0)
            if self.new_processlist() and sel.get_string(8).startswith('BACKGROUND'):
                mforms.Utilities.show_error("Error Killing Connection", "Thread %s cannot be killed" % connid, "OK",  "", "")
                return

            try:
                self.ctrl_be.exec_sql("KILL QUERY %s"%connid)
            except Exception as e:
                mforms.Utilities.show_error("Error Killing Connection", "Error executing KILL QUERY on thread %d: %s" % (connid, e), "OK",  "", "")
                break
          
        self.refresh()

