//
// $Id$
//
// SPProcessListController.m
// sequel-pro
//
// Created by Stuart Connolly (stuconnolly.com) on November 12, 2009
// Copyright (c) 2009 Stuart Connolly. All rights reserved.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// 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 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// More info at
#import
#import "SPProcessListController.h"
#import "SPArrayAdditions.h"
#import "TableDocument.h"
#import "SPConstants.h"
@interface SPProcessListController (PrivateAPI)
- (void)_getDatabaseProcessList;
- (void)_killProcessQueryWithId:(NSUInteger)processId;
- (void)_killProcessConnectionWithId:(NSUInteger)processId;
- (void)_updateServerProcessesFilterForFilterString:(NSString *)filterString;
@end
@implementation SPProcessListController
@synthesize connection;
/**
* Initialisation
*/
- (id)init
{
if ((self = [super initWithWindowNibName:@"DatabaseProcessList"])) {
processes = [[NSMutableArray alloc] init];
}
return self;
}
/**
* Interface initialisation
*/
- (void)awakeFromNib
{
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
// Set the process table view's vertical gridlines if required
[processListTableView setGridStyleMask:([prefs boolForKey:SPDisplayTableViewVerticalGridlines]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone];
// Set the strutcture and index view's font
BOOL useMonospacedFont = [prefs boolForKey:SPUseMonospacedFonts];
for (NSTableColumn *column in [processListTableView tableColumns])
{
[[column dataCell] setFont:(useMonospacedFont) ? [NSFont fontWithName:SPDefaultMonospacedFontName size:[NSFont smallSystemFontSize]] : [NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
}
// Register as an observer for the when the UseMonospacedFonts preference changes
[prefs addObserver:self forKeyPath:SPUseMonospacedFonts options:NSKeyValueObservingOptionNew context:NULL];
}
#pragma mark -
#pragma mark IBAction methods
/**
* Copies the currently selected process(es) to the pasteboard.
*/
- (IBAction)copy:(id)sender
{
NSResponder *firstResponder = [[self window] firstResponder];
if ((firstResponder == processListTableView) && ([processListTableView numberOfSelectedRows] > 0)) {
NSMutableString *string = [NSMutableString string];
NSIndexSet *rows = [processListTableView selectedRowIndexes];
NSUInteger i = [rows firstIndex];
while (i != NSNotFound)
{
if (i < [processesFiltered count]) {
NSDictionary *process = NSArrayObjectAtIndex(processesFiltered, i);
NSString *stringTmp = [NSString stringWithFormat:@"%@ %@ %@ %@ %@ %@ %@ %@",
[process objectForKey:@"Id"],
[process objectForKey:@"User"],
[process objectForKey:@"Host"],
[process objectForKey:@"db"],
[process objectForKey:@"Command"],
[process objectForKey:@"Time"],
[process objectForKey:@"State"],
[process objectForKey:@"Info"]];
[string appendString:stringTmp];
[string appendString:@"\n"];
}
i = [rows indexGreaterThanIndex:i];
}
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
// Copy the string to the pasteboard
[pasteBoard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil];
[pasteBoard setString:string forType:NSStringPboardType];
}
}
/**
* Close the process list sheet.
*/
- (IBAction)closeSheet:(id)sender
{
[NSApp endSheet:[self window] returnCode:[sender tag]];
[[self window] orderOut:self];
// If the filtered array is allocated and it's not a reference to the processes array get rid of it
if ((processesFiltered) && (processesFiltered != processes)) {
[processesFiltered release], processesFiltered = nil;
}
}
/**
* Refreshes the process list.
*/
- (IBAction)refreshProcessList:(id)sender
{
// Start progress Indicator
[refreshProgressIndicator startAnimation:self];
[refreshProgressIndicator setHidden:NO];
// Disable controls
[refreshProcessesButton setEnabled:NO];
[closeProcessListButton setEnabled:NO];
[saveProcessesButton setEnabled:NO];
[filterProcessesSearchField setEnabled:NO];
[self _getDatabaseProcessList];
// Reapply any filters is required
if ([[filterProcessesSearchField stringValue] length] > 0) {
[self _updateServerProcessesFilterForFilterString:[filterProcessesSearchField stringValue]];
}
[processListTableView reloadData];
// Enable controls
[filterProcessesSearchField setEnabled:YES];
[saveProcessesButton setEnabled:YES];
[closeProcessListButton setEnabled:YES];
[refreshProcessesButton setEnabled:YES];
// Stop progress Indicator
[refreshProgressIndicator stopAnimation:self];
[refreshProgressIndicator setHidden:YES];
}
/**
* Saves the process list to the selected file.
*/
- (IBAction)saveServerProcesses:(id)sender
{
NSSavePanel *panel = [NSSavePanel savePanel];
[panel setExtensionHidden:NO];
[panel setAllowsOtherFileTypes:YES];
[panel setCanSelectHiddenExtension:YES];
[panel beginSheetForDirectory:nil file:@"ServerProcesses" modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:nil];
}
/**
* Kills the currently selected process' query.
*/
- (IBAction)killProcessQuery:(id)sender
{
// No process selected. Interface validation should prevent this.
if ([processListTableView numberOfSelectedRows] != 1) return;
NSUInteger processId = [[[processes objectAtIndex:[processListTableView selectedRow]] valueForKey:@"Id"] integerValue];
NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Kill query?", @"kill query message")
defaultButton:NSLocalizedString(@"Kill", @"kill button")
alternateButton:NSLocalizedString(@"Cancel", @"cancel button")
otherButton:nil
informativeTextWithFormat:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to kill the current query executing on connection ID %lu?\n\nPlease be aware that continuing to kill this query may result in data corruption. Please proceed with caution.", @"kill query informative message"), (unsigned long)processId]];
NSArray *buttons = [alert buttons];
// Change the alert's cancel button to have the key equivalent of return
[[buttons objectAtIndex:0] setKeyEquivalent:@"k"];
[[buttons objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask];
[[buttons objectAtIndex:1] setKeyEquivalent:@"\r"];
[alert setAlertStyle:NSCriticalAlertStyle];
[alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:SPKillProcessQueryMode];
}
/**
* Kills the currently selected proceess' connection.
*/
- (IBAction)killProcessConnection:(id)sender
{
// No process selected. Interface validation should prevent this.
if ([processListTableView numberOfSelectedRows] != 1) return;
NSUInteger processId = [[[processes objectAtIndex:[processListTableView selectedRow]] valueForKey:@"Id"] integerValue];
NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Kill connection?", @"kill connection message")
defaultButton:NSLocalizedString(@"Kill", @"kill button")
alternateButton:NSLocalizedString(@"Cancel", @"cancel button")
otherButton:nil
informativeTextWithFormat:[NSString stringWithFormat:NSLocalizedString(@"Are you sure you want to kill connection ID %lu?\n\nPlease be aware that continuing to kill this connection may result in data corruption. Please proceed with caution.", @"kill connection informative message"), (unsigned long)processId]];
NSArray *buttons = [alert buttons];
// Change the alert's cancel button to have the key equivalent of return
[[buttons objectAtIndex:0] setKeyEquivalent:@"k"];
[[buttons objectAtIndex:0] setKeyEquivalentModifierMask:NSCommandKeyMask];
[[buttons objectAtIndex:1] setKeyEquivalent:@"\r"];
[alert setAlertStyle:NSCriticalAlertStyle];
[alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:SPKillProcessConnectionMode];
}
#pragma mark -
#pragma mark Other methods
/**
* Displays the process list sheet attached to the supplied window.
*/
- (void)displayProcessListSheetAttachedToWindow:(NSWindow *)window
{
// Weak reference
processesFiltered = processes;
// Get the current process list
[self _getDatabaseProcessList];
// Reload the tableview
[processListTableView reloadData];
// If the search field already has value from when the panel was previously open, apply the filter.
if ([[filterProcessesSearchField stringValue] length] > 0) {
[self _updateServerProcessesFilterForFilterString:[filterProcessesSearchField stringValue]];
}
// Open the sheet
[NSApp beginSheet:[self window] modalForWindow:window modalDelegate:self didEndSelector:nil contextInfo:nil];
}
/**
* Invoked when the kill alerts are dismissed. Decide what to do based on the user's decision.
*/
- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo
{
// Order out current sheet to suppress overlapping of sheets
if ([sheet respondsToSelector:@selector(orderOut:)])
[sheet orderOut:nil];
else if ([sheet respondsToSelector:@selector(window)])
[[sheet window] orderOut:nil];
if (returnCode == NSAlertDefaultReturn) {
NSUInteger processId = [[[processes objectAtIndex:[processListTableView selectedRow]] valueForKey:@"Id"] integerValue];
if ([contextInfo isEqualToString:SPKillProcessQueryMode]) {
[self _killProcessQueryWithId:processId];
}
else if ([contextInfo isEqualToString:SPKillProcessConnectionMode]) {
[self _killProcessConnectionWithId:processId];
}
}
}
/**
* Invoked when the save panel is dismissed.
*/
- (void)savePanelDidEnd:(NSSavePanel *)panel returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo
{
if (returnCode == NSOKButton) {
if ([processesFiltered count] > 0) {
NSMutableString *processesString = [NSMutableString stringWithFormat:@"# MySQL server proceese for %@\n\n", [(TableDocument *)[[NSApp mainWindow] delegate] host]];
for (NSDictionary *process in processesFiltered)
{
NSString *stringTmp = [NSString stringWithFormat:@"%@ %@ %@ %@ %@ %@ %@ %@",
[process objectForKey:@"Id"],
[process objectForKey:@"User"],
[process objectForKey:@"Host"],
[process objectForKey:@"db"],
[process objectForKey:@"Command"],
[process objectForKey:@"Time"],
[process objectForKey:@"State"],
[process objectForKey:@"Info"]];
[processesString appendString:stringTmp];
[processesString appendString:@"\n"];
}
[processesString writeToFile:[panel filename] atomically:YES encoding:NSUTF8StringEncoding error:NULL];
}
}
}
/**
* Menu item validation.
*/
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{
SEL action = [menuItem action];
if (action == @selector(copy:)) {
return ([processListTableView numberOfSelectedRows] > 0);
}
if ((action == @selector(killProcessQuery:)) || (action == @selector(killProcessConnection:))) {
return ([processListTableView numberOfSelectedRows] == 1);
}
return YES;
}
/**
* This method is called as part of Key Value Observing which is used to watch for prefernce changes which effect the interface.
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// Display table veiew vertical gridlines preference changed
if ([keyPath isEqualToString:SPDisplayTableViewVerticalGridlines]) {
[processListTableView setGridStyleMask:([[change objectForKey:NSKeyValueChangeNewKey] boolValue]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone];
}
// Use monospaced fonts preference changed
else if ([keyPath isEqualToString:SPUseMonospacedFonts]) {
BOOL useMonospacedFont = [[change objectForKey:NSKeyValueChangeNewKey] boolValue];
for (NSTableColumn *column in [processListTableView tableColumns])
{
[[column dataCell] setFont:(useMonospacedFont) ? [NSFont fontWithName:SPDefaultMonospacedFontName size:[NSFont smallSystemFontSize]] : [NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
}
[processListTableView reloadData];
}
}
#pragma mark -
#pragma mark Tableview delegate methods
/**
* Table view delegate method. Returns the number of rows in the table veiw.
*/
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [processesFiltered count];
}
/**
* Table view delegate method. Returns the specific object for the request column and row.
*/
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
id object = [[processesFiltered objectAtIndex:row] valueForKey:[tableColumn identifier]];
return (![object isNSNull]) ? object : @"NULL";
}
#pragma mark -
#pragma mark Text field delegate methods
/**
* Apply the filter string to the current process list.
*/
- (void)controlTextDidChange:(NSNotification *)notification
{
id object = [notification object];
if (object == filterProcessesSearchField) {
[self _updateServerProcessesFilterForFilterString:[object stringValue]];
}
}
#pragma mark -
/**
* Dealloc
*/
- (void)dealloc
{
[processes release], processes = nil;
[super dealloc];
}
@end
@implementation SPProcessListController (PrivateAPI)
/**
* Gets the current process list form the database;
*/
- (void)_getDatabaseProcessList
{
NSUInteger i = 0;
// Get processes
MCPResult *processList = [connection queryString:@"SHOW PROCESSLIST"];
[processList setReturnDataAsStrings:YES];
if ([processList numOfRows]) [processList dataSeek:0];
[processes removeAllObjects];
for (i = 0; i < [processList numOfRows]; i++)
{
[processes addObject:[processList fetchRowAsDictionary]];
}
}
/**
* Attempts to kill the query executing on the connection associate with the supplied ID.
*/
- (void)_killProcessQueryWithId:(NSUInteger)processId
{
// Kill the query
[connection queryString:[NSString stringWithFormat:@"KILL QUERY %lu", (unsigned long)processId]];
// Check for errors
if (![[connection getLastErrorMessage] isEqualToString:@""]) {
NSBeginAlertSheet(NSLocalizedString(@"Unable to kill query", @"error killing query message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [self window], self, nil, nil, nil,
[NSString stringWithFormat:NSLocalizedString(@"An error occured while attempting to kill the query associated with connection %lu.\n\nMySQL said: %@", @"error killing query informative message"), (unsigned long)processId, [connection getLastErrorMessage]]);
}
// Refresh the process list
[self refreshProcessList:self];
}
/**
* Attempts the kill the connection associated with the supplied ID.
*/
- (void)_killProcessConnectionWithId:(NSUInteger)processId
{
// Kill the connection
[connection queryString:[NSString stringWithFormat:@"KILL CONNECTION %lu", (unsigned long)processId]];
// Check for errors
if (![[connection getLastErrorMessage] isEqualToString:@""]) {
NSBeginAlertSheet(NSLocalizedString(@"Unable to kill connection", @"error killing connection message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [self window], self, nil, nil, nil,
[NSString stringWithFormat:NSLocalizedString(@"An error occured while attempting to kill connection %lu.\n\nMySQL said: %@", @"error killing query informative message"), (unsigned long)processId, [connection getLastErrorMessage]]);
}
// Refresh the process list
[self refreshProcessList:self];
}
/**
* Filter the displayed server processes against the supplied filter string.
*/
- (void)_updateServerProcessesFilterForFilterString:(NSString *)filterString
{
[saveProcessesButton setEnabled:NO];
filterString = [[filterString lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
// If the filtered array is allocated and its not a reference to the processes array,
// relase it to prevent memory leaks upon the next allocation.
if ((processesFiltered) && (processesFiltered != processes)) {
[processesFiltered release], processesFiltered = nil;
}
processesFiltered = [[NSMutableArray alloc] init];
if ([filterString length] == 0) {
[processesFiltered release];
processesFiltered = processes;
[saveProcessesButton setEnabled:YES];
[saveProcessesButton setTitle:@"Save As..."];
[processesCountTextField setStringValue:@""];
[processListTableView reloadData];
return;
}
// Perform filtering
for (NSDictionary *process in processes)
{
if (([[process objectForKey:@"Id"] rangeOfString:filterString options:NSCaseInsensitiveSearch].location != NSNotFound) ||
([[process objectForKey:@"User"] rangeOfString:filterString options:NSCaseInsensitiveSearch].location != NSNotFound) ||
([[process objectForKey:@"Host"] rangeOfString:filterString options:NSCaseInsensitiveSearch].location != NSNotFound) ||
((![[process objectForKey:@"db"] isNSNull]) && ([[process objectForKey:@"db"] rangeOfString:filterString options:NSCaseInsensitiveSearch].location != NSNotFound)) ||
([[process objectForKey:@"Command"] rangeOfString:filterString options:NSCaseInsensitiveSearch].location != NSNotFound) ||
([[process objectForKey:@"Time"] rangeOfString:filterString options:NSCaseInsensitiveSearch].location != NSNotFound) ||
((![[process objectForKey:@"State"] isNSNull]) && ([[process objectForKey:@"State"] rangeOfString:filterString options:NSCaseInsensitiveSearch].location != NSNotFound)) ||
((![[process objectForKey:@"Info"] isNSNull]) && ([[process objectForKey:@"Info"] rangeOfString:filterString options:NSCaseInsensitiveSearch].location != NSNotFound)))
{
[processesFiltered addObject:process];
}
}
[processListTableView reloadData];
[processesCountTextField setStringValue:[NSString stringWithFormat:NSLocalizedString(@"%lu of %lu", "filtered item count"), (unsigned long)[processesFiltered count], (unsigned long)[processes count]]];
[processesCountTextField setHidden:NO];
if ([processesFiltered count] == 0) return;
[saveProcessesButton setEnabled:YES];
[saveProcessesButton setTitle:@"Save View As..."];
}
@end