//
// $Id$
//
// CMMCPConnection.m
// sequel-pro
//
// Created by lorenz textor (lorenz@textor.ch) on Wed Sept 21 2005.
// Copyright (c) 2002-2003 Lorenz Textor. 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 "CMMCPConnection.h"
#import "SPStringAdditions.h"
#include
#include
static jmp_buf pingTimeoutJumpLocation;
static void forcePingTimeout(int signalNumber);
@interface CMMCPConnection(hidden)
- (void)getServerVersionString;
@end
@implementation CMMCPConnection(hidden)
- (void)getServerVersionString
{
if( mConnected ) {
CMMCPResult *theResult;
theResult = [self queryString:@"SHOW VARIABLES LIKE 'version'"];
if ([theResult numOfRows]) {
[theResult dataSeek:0];
serverVersionString = [[NSString stringWithString:[[theResult fetchRowAsArray] objectAtIndex:1]] retain];
}
}
}
@end
@implementation CMMCPConnection
/*
* Override the normal init methods, extending them to also init additional details,
* and to store details of the initialised connection to allow reconnection as method.
* Note this also behaves differently from the standard MCPKit connection methods -
* passwords are passed separately, and connections are not automatically made on init.
*/
- (id) init
{
[self initSPExtensions];
self = [super init];
serverVersionString = nil;
return self;
}
- (id) initToHost:(NSString *) host withLogin:(NSString *) login usingPort:(int) port
{
[self initSPExtensions];
self = [super init];
mEncoding = NSISOLatin1StringEncoding;
mConnection = mysql_init(mConnection);
mConnected = NO;
if (mConnection == NULL) {
[self autorelease];
return nil;
}
mConnectionFlags = kMCPConnectionDefaultOption;
connectionHost = [[NSString alloc] initWithString:host];
connectionLogin = [[NSString alloc] initWithString:login];
connectionPort = port;
connectionSocket = nil;
return self;
}
- (id) initToSocket:(NSString *) socket withLogin:(NSString *) login
{
[self initSPExtensions];
self = [super init];
mEncoding = NSISOLatin1StringEncoding;
mConnection = mysql_init(mConnection);
mConnected = NO;
if (mConnection == NULL) {
[self autorelease];
return nil;
}
mConnectionFlags = kMCPConnectionDefaultOption;
connectionHost = nil;
connectionLogin = [[NSString alloc] initWithString:login];
connectionSocket = [[NSString alloc] initWithString:socket];
connectionPort = 0;
return self;
}
/*
* Instantiate extra variables and load the connection error dialog for potential use.
*/
- (void) initSPExtensions
{
parentWindow = nil;
connectionHost = nil;
connectionLogin = nil;
connectionSocket = nil;
connectionPassword = nil;
connectionKeychainName = nil;
connectionKeychainAccount = nil;
keepAliveTimer = nil;
connectionTunnel = nil;
connectionTimeout = [[[NSUserDefaults standardUserDefaults] objectForKey:@"ConnectionTimeout"] intValue];
if (!connectionTimeout) connectionTimeout = 10;
useKeepAlive = [[[NSUserDefaults standardUserDefaults] objectForKey:@"UseKeepAlive"] doubleValue];
keepAliveInterval = [[[NSUserDefaults standardUserDefaults] objectForKey:@"KeepAliveInterval"] doubleValue];
if (!keepAliveInterval) keepAliveInterval = 0;
lastKeepAliveSuccess = nil;
connectionThreadId = 0;
maxAllowedPacketSize = -1;
lastQueryExecutionTime = 0;
lastQueryErrorId = 0;
lastQueryErrorMessage = nil;
lastQueryAffectedRows = 0;
if (![NSBundle loadNibNamed:@"ConnectionErrorDialog" owner:self]) {
NSLog(@"Connection error dialog could not be loaded; connection failure handling will not function correctly.");
}
willQueryStringSEL = @selector(willQueryString:);
stopKeepAliveTimerSEL = @selector(stopKeepAliveTimer);
startKeepAliveTimerResettingStateSEL = @selector(startKeepAliveTimerResettingState:);
cStringSEL = @selector(cStringFromString:);
cStringPtr = [self methodForSelector:cStringSEL];
stopKeepAliveTimerPtr = [self methodForSelector:stopKeepAliveTimerSEL];
startKeepAliveTimerResettingStatePtr = [self methodForSelector:startKeepAliveTimerResettingStateSEL];
}
/*
* Sets the password to be stored locally.
* Providing a keychain name is much more secure.
*/
- (BOOL) setPassword:(NSString *)thePassword
{
if (connectionPassword) [connectionPassword release], connectionPassword = nil;
if (connectionKeychainName) [connectionKeychainName release], connectionKeychainName = nil;
if (connectionKeychainAccount) [connectionKeychainAccount release], connectionKeychainAccount = nil;
connectionPassword = [[NSString alloc] initWithString:thePassword];
return YES;
}
/*
* Sets the keychain name to use to retrieve the password. This is the recommended and
* secure way of supplying a password to the SSH tunnel.
*/
- (BOOL) setPasswordKeychainName:(NSString *)theName account:(NSString *)theAccount
{
if (connectionPassword) [connectionPassword release], connectionPassword = nil;
if (connectionKeychainName) [connectionKeychainName release], connectionKeychainName = nil;
if (connectionKeychainAccount) [connectionKeychainAccount release], connectionKeychainAccount = nil;
connectionKeychainName = [[NSString alloc] initWithString:theName];
connectionKeychainAccount = [[NSString alloc] initWithString:theAccount];
return YES;
}
/*
* Set a SSH tunnel object to connect through. This object will be retained locally,
* and will be automatically connected/connection checked/reconnected/disconnected
* together with the main connection.
*/
- (BOOL) setSSHTunnel:(SPSSHTunnel *)theTunnel
{
connectionTunnel = theTunnel;
[connectionTunnel retain];
currentSSHTunnelState = [connectionTunnel state];
[connectionTunnel setConnectionStateChangeSelector:@selector(sshTunnelStateChange:) delegate:self];
return YES;
}
/*
* Add a new connection method, intended for use with the init methods above.
* Uses the stored details to instantiate a connection to the specified server,
* including custom timeouts - used for pings, not for long-running commands.
*/
- (BOOL) connect
{
const char *theLogin = [self cStringFromString:connectionLogin];
const char *theHost;
const char *thePass;
const char *theSocket;
void *theRet;
// Ensure that a password method has been provided
if (connectionKeychainName == nil && connectionPassword == nil) return NO;
// Start the keepalive timer
[self startKeepAliveTimerResettingState:YES];
// Disconnect if a connection is already active
if (mConnected) {
[self disconnect];
mConnection = mysql_init(NULL);
if (mConnection == NULL) return NO;
}
// Ensure the custom timeout option is set
if (mConnection != NULL) {
mysql_options(mConnection, MYSQL_OPT_CONNECT_TIMEOUT, (const void *)&connectionTimeout);
}
// Set the host as appropriate
if (!connectionHost || ![connectionHost length]) {
theHost = NULL;
} else {
theHost = [self cStringFromString:connectionHost];
}
// Use the default socket if none is set, or set appropriately
if (connectionSocket == nil || ![connectionSocket length]) {
theSocket = kMCPConnectionDefaultSocket;
} else {
theSocket = [self cStringFromString:connectionSocket];
}
// Select the password from the provided method
if (connectionKeychainName) {
KeyChain *keychain;
keychain = [[KeyChain alloc] init];
thePass = [self cStringFromString:[keychain getPasswordForName:connectionKeychainName account:connectionKeychainAccount]];
[keychain release];
} else {
thePass = [self cStringFromString:connectionPassword];
}
// Connect
theRet = mysql_real_connect(mConnection, theHost, theLogin, thePass, NULL, connectionPort, theSocket, mConnectionFlags);
thePass = NULL;
if (theRet != mConnection) {
[self setLastErrorMessage:nil];
if (connectionTunnel) {
[connectionTunnel disconnect];
[delegate setStatusIconToImageWithName:@"ssh-disconnected"];
}
return mConnected = NO;
}
mConnected = YES;
mEncoding = [MCPConnection encodingForMySQLEncoding:mysql_character_set_name(mConnection)];
connectionThreadId = mConnection->thread_id;
[self timeZone]; // Getting the timezone used by the server.
isMaxAllowedPacketEditable = [self isMaxAllowedPacketEditable];
if (![self fetchMaxAllowedPacket]) {
[self setLastErrorMessage:nil];
if (connectionTunnel) {
[connectionTunnel disconnect];
[delegate setStatusIconToImageWithName:@"ssh-disconnected"];
}
return mConnected = NO;
}
// Register notification if a query was sent to the MySQL connection
// to be able to identify the sender of that query
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willPerformQuery:)
name:@"SMySQLQueryWillBePerformed" object:nil];
[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"ConsoleEnableLogging" options:NSKeyValueObservingOptionNew context:NULL];
// Init 'consoleLoggingEnabled'
consoleLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"ConsoleEnableLogging"];
return mConnected;
}
/*
* Override the stored disconnection method to ensure that disconnecting clears stored timers.
*/
- (void) disconnect
{
[super disconnect];
if (connectionTunnel) {
[connectionTunnel disconnect];
[delegate setStatusIconToImageWithName:@"ssh-disconnected"];
}
if( serverVersionString != nil ) {
[serverVersionString release];
serverVersionString = nil;
}
[self stopKeepAliveTimer];
}
/*
* return the server major version or -1 on fail
*/
- (int)serverMajorVersion
{
if( mConnected ) {
if( serverVersionString == nil ) {
[self getServerVersionString];
}
if( serverVersionString != nil ) {
return [[[serverVersionString componentsSeparatedByString:@"."] objectAtIndex:0] intValue];
}
}
return -1;
}
/*
* return the server minor version or -1 on fail
*/
- (int)serverMinorVersion
{
if( mConnected ) {
if( serverVersionString == nil ) {
[self getServerVersionString];
}
if( serverVersionString != nil ) {
return [[[serverVersionString componentsSeparatedByString:@"."] objectAtIndex:1] intValue];
}
}
return -1;
}
/*
* return the server release version or -1 on fail
*/
- (int)serverReleaseVersion
{
if( mConnected ) {
if( serverVersionString == nil ) {
[self getServerVersionString];
}
if( serverVersionString != nil ) {
NSString *s = [[serverVersionString componentsSeparatedByString:@"."] objectAtIndex:2];
return [[[s componentsSeparatedByString:@"-"] objectAtIndex:0] intValue];
}
}
return -1;
}
/*
* Reconnect to the currently "active" - but possibly disconnected - connection, using the
* stored details.
* Error checks extensively - if this method fails, it will ask how to proceed and loop depending
* on the status, not returning control until either a connection has been established or
* the connection and document have been closed.
*/
- (BOOL) reconnect
{
NSString *currentEncoding = nil;
BOOL currentEncodingUsesLatin1Transport = NO;
NSString *currentDatabase = nil;
// Store the current database and encoding so they can be re-set if reconnection was successful
if (delegate && [delegate valueForKey:@"selectedDatabase"]) {
currentDatabase = [NSString stringWithString:[delegate valueForKey:@"selectedDatabase"]];
}
if (delegate && [delegate valueForKey:@"_encoding"]) {
currentEncoding = [NSString stringWithString:[delegate valueForKey:@"_encoding"]];
}
if (delegate && [delegate respondsToSelector:@selector(connectionEncodingViaLatin1)]) {
currentEncodingUsesLatin1Transport = [delegate connectionEncodingViaLatin1];
}
// Close the connection if it exists.
if (mConnected) {
mysql_close(mConnection);
mConnection = NULL;
}
mConnected = NO;
// If there is a tunnel, ensure it's disconnected and attempt to reconnect it in blocking fashion
if (connectionTunnel) {
[connectionTunnel setConnectionStateChangeSelector:nil delegate:nil];
if ([connectionTunnel state] != SPSSH_STATE_IDLE) [connectionTunnel disconnect];
[connectionTunnel connect];
[delegate setStatusIconToImageWithName:@"ssh-connecting"];
NSDate *tunnelStartDate = [NSDate date], *interfaceInteractionTimer;
// Allow the tunnel to attempt to connect in a loop
while (1) {
if ([connectionTunnel state] == SPSSH_STATE_CONNECTED) {
[delegate setStatusIconToImageWithName:@"ssh-connected"];
connectionPort = [connectionTunnel localPort];
break;
}
if ([[NSDate date] timeIntervalSinceDate:tunnelStartDate] > (connectionTimeout + 1)) {
[connectionTunnel disconnect];
[delegate setStatusIconToImageWithName:@"ssh-disconnected"];
break;
}
// Process events for a short time, allowing dialogs to be shown but waiting for the tunnel
interfaceInteractionTimer = [NSDate date];
[[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
tunnelStartDate = [tunnelStartDate addTimeInterval:([[NSDate date] timeIntervalSinceDate:interfaceInteractionTimer] - 0.25)];
}
currentSSHTunnelState = [connectionTunnel state];
[connectionTunnel setConnectionStateChangeSelector:@selector(sshTunnelStateChange:) delegate:self];
}
if (!connectionTunnel || [connectionTunnel state] == SPSSH_STATE_CONNECTED) {
// Attempt to reinitialise the connection - if this fails, it will still be set to NULL.
if (mConnection == NULL) {
mConnection = mysql_init(NULL);
}
if (mConnection != NULL) {
// Set a connection timeout for the new connection
mysql_options(mConnection, MYSQL_OPT_CONNECT_TIMEOUT, (const void *)&connectionTimeout);
// Attempt to reestablish the connection
[self connect];
}
}
// If the connection was successfully established, reselect the old database and encoding if appropriate.
if (mConnected) {
if (currentDatabase) {
[self selectDB:currentDatabase];
}
if (currentEncoding) {
[self queryString:[NSString stringWithFormat:@"/*!40101 SET NAMES '%@' */", currentEncoding]];
[self setEncoding:[CMMCPConnection encodingForMySQLEncoding:[currentEncoding UTF8String]]];
if (currentEncodingUsesLatin1Transport) {
[self queryString:@"/*!40101 SET CHARACTER_SET_RESULTS=latin1 */"];
}
}
} else if (parentWindow) {
[self setLastErrorMessage:nil];
// If the connection was not successfully established, ask how to proceed.
[NSApp beginSheet:connectionErrorDialog modalForWindow:parentWindow modalDelegate:self didEndSelector:nil contextInfo:nil];
int connectionErrorCode = [NSApp runModalForWindow:connectionErrorDialog];
[NSApp endSheet:connectionErrorDialog];
[connectionErrorDialog orderOut:nil];
switch (connectionErrorCode) {
// Should disconnect
case 2:
[parentWindow close];
return NO;
// Should retry
default:
return [self reconnect];
}
}
return mConnected;
}
/*
* Set the parent window of the connection for use with dialogs.
*/
- (void)setParentWindow:(NSWindow *)theWindow
{
parentWindow = theWindow;
}
/*
* Handle any state changes in the associated SSH Tunnel
*/
- (void)sshTunnelStateChange:(SPSSHTunnel *)theTunnel
{
int newState = [theTunnel state];
if (delegate && [delegate respondsToSelector:@selector(setStatusIconToImageWithName:)]) {
if (newState == SPSSH_STATE_IDLE) [delegate setStatusIconToImageWithName:@"ssh-disconnected"];
else if (newState == SPSSH_STATE_CONNECTED) [delegate setStatusIconToImageWithName:@"ssh-connected"];
else [delegate setStatusIconToImageWithName:@"ssh-connecting"];
}
// Restart the tunnel if it dies
if (mConnected && newState == SPSSH_STATE_IDLE && currentSSHTunnelState == SPSSH_STATE_CONNECTED) {
currentSSHTunnelState = newState;
[connectionTunnel setConnectionStateChangeSelector:nil delegate:nil];
[self reconnect];
return;
}
currentSSHTunnelState = newState;
}
/*
* Ends an existing modal session
*/
- (IBAction) closeSheet:(id)sender
{
[NSApp stopModalWithCode:[sender tag]];
}
/*
* Determines whether a supplied error number can be classed as a connection
* error.
*/
+ (BOOL) isErrorNumberConnectionError:(int)theErrorNumber
{
switch (theErrorNumber) {
case 2001: // CR_SOCKET_CREATE_ERROR
case 2002: // CR_CONNECTION_ERROR
case 2003: // CR_CONN_HOST_ERROR
case 2004: // CR_IPSOCK_ERROR
case 2005: // CR_UNKNOWN_HOST
case 2006: // CR_SERVER_GONE_ERROR
case 2007: // CR_VERSION_ERROR
case 2009: // CR_WRONG_HOST_INFO
case 2012: // CR_SERVER_HANDSHAKE_ERR
case 2013: // CR_SERVER_LOST
case 2027: // CR_MALFORMED_PACKET
case 2032: // CR_DATA_TRUNCATED
case 2047: // CR_CONN_UNKNOW_PROTOCOL
case 2048: // CR_INVALID_CONN_HANDLE
case 2050: // CR_FETCH_CANCELED
case 2055: // CR_SERVER_LOST_EXTENDED
return YES;
}
return NO;
}
/*
* Gets a proper NSStringEncoding according to the given MySQL charset.
*
* MySQL 4.0 offers this charsets:
* big5 cp1251 cp1257 croat czech danish dec8 dos estonia euc_kr gb2312 gbk german1
* greek hebrew hp8 hungarian koi8_ru koi8_ukr latin1 latin1_de latin2 latin5 sjis
* swe7 tis620 ujis usa7 win1250 win1251ukr
*
* WARNING : incomplete implementation. Please, send your fixes.
*/
+ (NSStringEncoding) encodingForMySQLEncoding:(const char *) mysqlEncoding
{
// Unicode encodings:
if (!strncmp(mysqlEncoding, "utf8", 4)) {
return NSUTF8StringEncoding;
}
if (!strncmp(mysqlEncoding, "ucs2", 4)) {
return NSUnicodeStringEncoding;
}
// Roman alphabet encodings:
if (!strncmp(mysqlEncoding, "ascii", 5)) {
return NSASCIIStringEncoding;
}
if (!strncmp(mysqlEncoding, "latin1", 6)) {
return NSISOLatin1StringEncoding;
}
if (!strncmp(mysqlEncoding, "macroman", 8)) {
return NSMacOSRomanStringEncoding;
}
// Roman alphabet with central/east european additions:
if (!strncmp(mysqlEncoding, "latin2", 6)) {
return NSISOLatin2StringEncoding;
}
if (!strncmp(mysqlEncoding, "cp1250", 6)) {
return NSWindowsCP1250StringEncoding;
}
if (!strncmp(mysqlEncoding, "win1250", 7)) {
return NSWindowsCP1250StringEncoding;
}
if (!strncmp(mysqlEncoding, "cp1257", 6)) {
return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingWindowsBalticRim);
}
// Additions for Turkish:
if (!strncmp(mysqlEncoding, "latin5", 6)) {
return NSWindowsCP1254StringEncoding;
}
// Greek:
if (!strncmp(mysqlEncoding, "greek", 5)) {
return NSWindowsCP1253StringEncoding;
}
// Cyrillic:
if (!strncmp(mysqlEncoding, "win1251ukr", 6)) {
return NSWindowsCP1251StringEncoding;
}
if (!strncmp(mysqlEncoding, "cp1251", 6)) {
return NSWindowsCP1251StringEncoding;
}
if (!strncmp(mysqlEncoding, "koi8_ru", 6)) {
return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingKOI8_R);
}
if (!strncmp(mysqlEncoding, "koi8_ukr", 6)) {
return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingKOI8_R);
}
// Arabic:
if (!strncmp(mysqlEncoding, "cp1256", 6)) {
return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingWindowsArabic);
}
// Hebrew:
if (!strncmp(mysqlEncoding, "hebrew", 6)) {
CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingISOLatinHebrew);
}
// Asian:
if (!strncmp(mysqlEncoding, "ujis", 4)) {
return NSJapaneseEUCStringEncoding;
}
if (!strncmp(mysqlEncoding, "sjis", 4)) {
return NSShiftJISStringEncoding;
}
if (!strncmp(mysqlEncoding, "big5", 4)) {
return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingBig5);
}
if (!strncmp(mysqlEncoding, "euc_kr", 6)) {
return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingEUC_KR);
}
if (!strncmp(mysqlEncoding, "euckr", 5)) {
return CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingEUC_KR);
}
// Default to iso latin 1, even if it is not exact (throw an exception?)
NSLog(@"WARNING : unknown name for MySQL encoding '%s'!\n\t\tFalling back to iso-latin1.", mysqlEncoding);
return NSISOLatin1StringEncoding;
}
/*
* Modified version of selectDB to be used in Sequel Pro.
* Checks the connection exists, and handles keepalive, otherwise calling the parent implementation.
*/
- (BOOL) selectDB:(NSString *) dbName
{
if (!mConnected) return NO;
[self stopKeepAliveTimer];
if (![self checkConnection]) return NO;
if ([super selectDB:dbName]) {
[self startKeepAliveTimerResettingState:YES];
return YES;
}
[self setLastErrorMessage:nil];
if (connectionTunnel) {
[connectionTunnel disconnect];
[delegate setStatusIconToImageWithName:@"ssh-disconnected"];
}
return NO;
}
/*
* Via that method the current mySQLConnection will be informed
* which object sent the current query.
*/
- (void)willPerformQuery:(NSNotification *)notification
{
// If the sender was CustomQuery disable the retry of queries.
// TODO: maybe there's a better way
if( [[[[notification object] class] description] isEqualToString:@"CustomQuery"] ) {
retryAllowed = NO;
} else {
retryAllowed = 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
{
if ([keyPath isEqualToString:@"ConsoleEnableLogging"]) {
consoleLoggingEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"ConsoleEnableLogging"];
}
}
/*
* Override the standard queryString: method to default to the connection encoding, as before,
* before pssing on to queryString: usingEncoding:.
*/
- (CMMCPResult *)queryString:(NSString *) query
{
return [self queryString:query usingEncoding:mEncoding];
}
/*
* Modified version of queryString to be used in Sequel Pro.
* Error checks connection extensively - if this method fails due to a connection error, it will ask how to
* proceed and loop depending on the status, not returning control until either the query has been executed
* and the result can be returned or the connection and document have been closed.
*/
- (CMMCPResult *)queryString:(NSString *) query usingEncoding:(NSStringEncoding) encoding
{
CMMCPResult *theResult = nil;
int queryStartTime;
const char *theCQuery;
unsigned long theCQueryLength;
int queryResultCode;
int queryErrorId = 0;
my_ulonglong queryAffectedRows = 0;
int currentMaxAllowedPacket = -1;
BOOL isQueryRetry = NO;
NSString *queryErrorMessage = nil;
// If no connection is present, return nil.
if (!mConnected) {
// Write a log entry
if ([delegate respondsToSelector:@selector(queryGaveError:)]) [delegate queryGaveError:@"No connection available!"];
// Notify that the query has been performed
[[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self];
// Show an error alert while resetting
NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), @"No connection available!",
nil, nil, [delegate valueForKeyPath:@"tableWindow"], self, nil, nil, nil, @"No connection available!");
return nil;
}
(void)(*stopKeepAliveTimerPtr)(self, stopKeepAliveTimerSEL);
// queryStartTime = clock();
// Inform the delegate about the query if logging is enabled and
// delegate responds to willQueryString:
if (consoleLoggingEnabled && delegateResponseToWillQueryString)
(void)(NSString*)(*willQueryStringPtr)(delegate, willQueryStringSEL, query);
// Derive the query string in the correct encoding
NSData *d = NSStringDataUsingLossyEncoding(query, encoding, 1);
theCQuery = [d bytes];
// Set the length of the current query
theCQueryLength = [d length];
// Check query length against max_allowed_packet; if it is larger, the
// query would error, so if max_allowed_packet is editable for the user
// increase it for the current session and reconnect.
if(maxAllowedPacketSize < theCQueryLength) {
if(isMaxAllowedPacketEditable) {
currentMaxAllowedPacket = maxAllowedPacketSize;
[self setMaxAllowedPacketTo:strlen(theCQuery)+1024 resetSize:NO];
[self reconnect];
} else {
NSString *errorMessage = [NSString stringWithFormat:NSLocalizedString(@"The query length of %d bytes is larger than max_allowed_packet size (%d).",
@"error message if max_allowed_packet < query size"),
theCQueryLength, maxAllowedPacketSize];
// Write a log entry and update the connection error messages for those uses that check it
if ([delegate respondsToSelector:@selector(queryGaveError:)]) [delegate queryGaveError:errorMessage];
[self setLastErrorMessage:errorMessage];
// Notify that the query has been performed
[[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self];
// Show an error alert while resetting
NSBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"),
nil, nil, [delegate valueForKeyPath:@"tableWindow"], self, nil, nil, nil, errorMessage);
return nil;
}
}
// In a loop to allow one reattempt, perform the query.
while (1) {
// If this query has failed once already, check the connection
if (isQueryRetry) {
if (![self checkConnection]) {
// Notify that the query has been performed
[[NSNotificationCenter defaultCenter] postNotificationName:@"SMySQLQueryHasBeenPerformed" object:self];
return nil;
}
}
// Run (or re-run) the query, timing the execution time of the query - note
// that this time will include network lag.
queryStartTime = clock();
queryResultCode = mysql_real_query(mConnection, theCQuery, theCQueryLength);
lastQueryExecutionTime = (clock() - queryStartTime);
// On success, capture the results
if (0 == queryResultCode) {
if (mysql_field_count(mConnection) != 0) {
theResult = [[CMMCPResult alloc] initWithMySQLPtr:mConnection encoding:mEncoding timeZone:mTimeZone];
// Ensure no problem occurred during the result fetch
if (mysql_errno(mConnection) != 0) {
queryErrorMessage = [[NSString alloc] initWithString:[self stringWithCString:mysql_error(mConnection)]];
queryErrorId = mysql_errno(mConnection);
break;
}
}
queryErrorMessage = [[NSString alloc] initWithString:@""];
queryErrorId = 0;
queryAffectedRows = mysql_affected_rows(mConnection);
// On failure, set the error messages and IDs
} else {
queryErrorMessage = [[NSString alloc] initWithString:[self stringWithCString:mysql_error(mConnection)]];
queryErrorId = mysql_errno(mConnection);
// If the error was a connection error, retry once
if (!isQueryRetry && retryAllowed && [CMMCPConnection isErrorNumberConnectionError:queryErrorId]) {
isQueryRetry = YES;
continue;
}
}
break;
}
// If the mysql thread id has changed as a result of a connection error,
// ensure connection details are still correct
if (connectionThreadId != mConnection->thread_id) [self restoreConnectionDetails];
// If max_allowed_packet was changed, reset it to default
if(currentMaxAllowedPacket > -1)
[self setMaxAllowedPacketTo:currentMaxAllowedPacket resetSize:YES];
// Update error strings and IDs
lastQueryErrorId = queryErrorId;
[self setLastErrorMessage:queryErrorMessage?queryErrorMessage:@""];
if (queryErrorMessage) [queryErrorMessage release];
lastQueryAffectedRows = queryAffectedRows;
// If an error occurred, inform the delegate
if (queryResultCode & delegateResponseToWillQueryString)
[delegate queryGaveError:lastQueryErrorMessage];
(void)(int)(*startKeepAliveTimerResettingStatePtr)(self, startKeepAliveTimerResettingStateSEL, YES);
if (!theResult) return nil;
return [theResult autorelease];
}
/*
* Return the time taken to execute the last query. This should be close to the time it took
* the server to run the query, but will include network lag and some client library overhead.
*/
- (float) lastQueryExecutionTime
{
return lastQueryExecutionTime;
}
/*
* Modified version of selectDB to be used in Sequel Pro.
* Checks the connection exists, and handles keepalive, otherwise calling the parent implementation.
*/
- (MCPResult *) listDBsLike:(NSString *) dbsName
{
if (!mConnected) return NO;
[self stopKeepAliveTimer];
if (![self checkConnection]) return [[[MCPResult alloc] init] autorelease];
[self startKeepAliveTimerResettingState:YES];
return [super listDBsLike:dbsName];
}
/*
* Checks whether the connection to the server is still active. If not, prompts for what approach to take,
* offering to retry, reconnect or disconnect the connection.
*/
- (BOOL)checkConnection
{
if (!mConnected) return NO;
BOOL connectionVerified = FALSE;
// Check whether the connection is still operational via a wrapped version of MySQL ping.
connectionVerified = [self pingConnection];
// If the connection doesn't appear to be responding, show a dialog asking how to proceed
if (!connectionVerified) {
[NSApp beginSheet:connectionErrorDialog modalForWindow:parentWindow modalDelegate:self didEndSelector:nil contextInfo:nil];
int responseCode = [NSApp runModalForWindow:connectionErrorDialog];
[NSApp endSheet:connectionErrorDialog];
[connectionErrorDialog orderOut:nil];
switch (responseCode) {
// "Reconnect" has been selected. Request a reconnect, and retry.
case 1:
[self reconnect];
return [self checkConnection];
// "Disconnect" has been selected. Close the parent window, which will handle disconnections, and return false.
case 2:
[parentWindow close];
return FALSE;
// "Retry" has been selected - return a recursive call.
default:
return [self checkConnection];
}
// If a connection exists, check whether the thread id differs; if so, the connection has
// probably been reestablished and we need to reset the connection encoding
} else if (connectionThreadId != mConnection->thread_id) [self restoreConnectionDetails];
return connectionVerified;
}
/**
* Restore the connection encoding details as necessary based on the delegate-provided
* details.
*/
- (void) restoreConnectionDetails
{
connectionThreadId = mConnection->thread_id;
[self fetchMaxAllowedPacket];
if (delegate && [delegate valueForKey:@"_encoding"]) {
[self queryString:[NSString stringWithFormat:@"/*!40101 SET NAMES '%@' */", [NSString stringWithString:[delegate valueForKey:@"_encoding"]]]];
if (delegate && [delegate respondsToSelector:@selector(connectionEncodingViaLatin1)]) {
if ([delegate connectionEncodingViaLatin1]) [self queryString:@"/*!40101 SET CHARACTER_SET_RESULTS=latin1 */"];
}
}
}
- (void)setDelegate:(id)object
{
delegate = object;
delegateResponseToWillQueryString = (delegate && [delegate respondsToSelector:willQueryStringSEL]);
willQueryStringPtr = [delegate methodForSelector:willQueryStringSEL];
}
/* Getting the currently used time zone (in communication with the DB server). */
/* fixes mysql 4.1.14 problem, can be deleted as soon as fixed in the framework */
- (NSTimeZone *)timeZone
{
if ([self checkConnection]) {
MCPResult *theSessionTZ = [self queryString:@"SHOW VARIABLES LIKE '%time_zone'"];
NSArray *theRow;
id theTZName;
NSTimeZone *theTZ;
[theSessionTZ dataSeek:1ULL];
theRow = [theSessionTZ fetchRowAsArray];
theTZName = [theRow objectAtIndex:1];
if ( [theTZName isKindOfClass:[NSData class]] ) {
// MySQL 4.1.14 returns the mysql variables as NSData
theTZName = [self stringWithText:theTZName];
}
if ([theTZName isEqualToString:@"SYSTEM"]) {
[theSessionTZ dataSeek:0ULL];
theRow = [theSessionTZ fetchRowAsArray];
theTZName = [theRow objectAtIndex:1];
if ( [theTZName isKindOfClass:[NSData class]] ) {
// MySQL 4.1.14 returns the mysql variables as NSData
theTZName = [self stringWithText:theTZName];
}
}
if (theTZName) { // Old versions of the server does not support there own time zone ?
theTZ = [NSTimeZone timeZoneWithName:theTZName];
} else {
// By default set the time zone to the local one..
// Try to get the name using the previously available variable:
theSessionTZ = [self queryString:@"SHOW VARIABLES LIKE 'timezone'"];
[theSessionTZ dataSeek:0ULL];
theRow = [theSessionTZ fetchRowAsArray];
theTZName = [theRow objectAtIndex:1];
if (theTZName) {
// Finally we found one ...
theTZ = [NSTimeZone timeZoneWithName:theTZName];
} else {
theTZ = [NSTimeZone defaultTimeZone];
//theTZ = [NSTimeZone systemTimeZone];
NSLog(@"The time zone is not defined on the server, set it to the default one : %@", theTZ);
}
}
if (theTZ != mTimeZone) {
[mTimeZone release];
mTimeZone = [theTZ retain];
}
}
return mTimeZone;
}
/*
* The current versions of MCPKit (and up to and including 3.0.1) use MySQL 4.1.12; this has an issue with
* mysql_ping where a connection which is terminated will cause mysql_ping never to respond, even when
* connection timeouts are set. Full details of this issue are available at http://bugs.mysql.com/bug.php?id=9678 ;
* this bug was fixed in 4.1.22 and later versions.
* This issue can be replicated by connecting to a remote host, and then configuring a firewall on that host
* to drop all packets on the connected port - mysql_ping and so Sequel Pro will hang.
* Until the client libraries are updated, this provides a drop-in wrapper for mysql_ping, which calls mysql_ping
* while running a SIGALRM to enforce the specified connection time. This is low-level but effective.
* Unlike mysql_ping, this function returns FALSE on failure and TRUE on success.
*/
- (BOOL) pingConnection
{
struct sigaction timeoutAction;
NSDate *startDate = [[NSDate alloc] initWithTimeIntervalSinceNow:0];
BOOL pingSuccess = FALSE;
// Construct the SIGALRM to fire after the connection timeout if it isn't cleared, calling the forcePingTimeout function.
timeoutAction.sa_handler = forcePingTimeout;
sigemptyset(&timeoutAction.sa_mask);
timeoutAction.sa_flags = 0;
sigaction(SIGALRM, &timeoutAction, NULL);
alarm(connectionTimeout+1);
// Set up a "restore point", returning 0; if longjmp is used later with this reference, execution
// jumps back to this point and returns a nonzero value, so this function evaluates to false when initially
// set and true if it's called again.
if (setjmp(pingTimeoutJumpLocation)) {
// The connection timed out - we want to return false.
pingSuccess = FALSE;
// On direct execution:
} else {
// Run mysql_ping, which returns 0 on success, and otherwise an error.
pingSuccess = (BOOL)(! mysql_ping(mConnection));
// If the ping failed within a second, try another one; this is because a terminated-but-then
// restored connection is at times restored or functional after a ping, but the ping still returns
// an error. This additional check ensures the returned status is correct with minimal other effect.
if (!pingSuccess && ([startDate timeIntervalSinceNow] > -1)) {
pingSuccess = (BOOL)(! mysql_ping(mConnection));
}
}
// Reset and clear the SIGALRM used to check connection timeouts.
alarm(0);
timeoutAction.sa_handler = SIG_IGN;
sigemptyset(&timeoutAction.sa_mask);
timeoutAction.sa_flags = 0;
sigaction(SIGALRM, &timeoutAction, NULL);
[startDate release];
return pingSuccess;
}
/*
* This function is paired with pingConnection, and provides a method of enforcing the connection
* timeout when mysql_ping does not respect the specified limits.
*/
static void forcePingTimeout(int signalNumber)
{
longjmp(pingTimeoutJumpLocation, 1);
}
/*
* Restarts a keepalive to fire in the future.
*/
- (void) startKeepAliveTimerResettingState:(BOOL)resetState
{
if (keepAliveTimer) [self stopKeepAliveTimer];
if (!mConnected) return;
if (resetState && lastKeepAliveSuccess) {
[lastKeepAliveSuccess release];
lastKeepAliveSuccess = nil;
}
if (useKeepAlive && keepAliveInterval) {
keepAliveTimer = [NSTimer
scheduledTimerWithTimeInterval:keepAliveInterval
target:self
selector:@selector(keepAlive:)
userInfo:nil
repeats:NO];
[keepAliveTimer retain];
}
}
/*
* Stops a keepalive if one is set for the future.
*/
- (void) stopKeepAliveTimer
{
if (!keepAliveTimer) return;
[keepAliveTimer invalidate];
[keepAliveTimer release];
keepAliveTimer = nil;
}
/*
* Keeps a connection alive by running a ping.
*/
- (void) keepAlive:(NSTimer *)theTimer
{
if (!mConnected) return;
// If there a successful keepalive record exists, and it was more than 5*keepaliveinterval ago,
// abort. This prevents endless spawning of threads in a state where the connection has been
// cut but mysql doesn't pick up on the fact - see comment for pingConnection above. The same
// forced-timeout approach cannot be used here on a background thread.
// When the connection is disconnected in code, these 5 "hanging" threads are automatically cleaned.
if (lastKeepAliveSuccess && [lastKeepAliveSuccess timeIntervalSinceNow] < -5 * keepAliveInterval) return;
[NSThread detachNewThreadSelector:@selector(threadedKeepAlive) toTarget:self withObject:nil];
[self startKeepAliveTimerResettingState:NO];
}
/*
* A threaded keepalive to avoid blocking the interface
*/
- (void) threadedKeepAlive
{
if (!mConnected) return;
mysql_ping(mConnection);
if (lastKeepAliveSuccess) {
[lastKeepAliveSuccess release];
lastKeepAliveSuccess = nil;
}
lastKeepAliveSuccess = [[NSDate alloc] initWithTimeIntervalSinceNow:0];
}
/*
* Modified version of the original to support a supplied encoding.
* For internal use only. Transforms a NSString to a C type string (ending with \0).
* Lossy conversions are enabled.
*/
- (const char *) cStringFromString:(NSString *) theString usingEncoding:(NSStringEncoding) encoding
{
NSMutableData *theData;
if (! theString) {
return (const char *)NULL;
}
theData = [NSMutableData dataWithData:[theString dataUsingEncoding:encoding allowLossyConversion:YES]];
[theData increaseLengthBy:1];
return (const char *)[theData bytes];
}
/*
* Returns a string for the last MySQL error message on the connection.
* This is cached within the object to allow helper queries to be performed
* without affecting the state that the GUI is querying.
*/
- (NSString *) getLastErrorMessage
{
return lastQueryErrorMessage;
}
/*
* Sets the string for the last MySQL error message on the connection,
* managing memory as appropriate. Supply a nil string to store the
* last error on the connection.
*/
- (void) setLastErrorMessage:(NSString *)theErrorMessage
{
if (!theErrorMessage) theErrorMessage = [self stringWithCString:mysql_error(mConnection)];
if (lastQueryErrorMessage) [lastQueryErrorMessage release], lastQueryErrorMessage = nil;
lastQueryErrorMessage = [[NSString alloc] initWithString:theErrorMessage];
}
/*
* Returns the ErrorID of the last MySQL error on the connection.
* This is cached within the object to allow helper queries to be performed
* without affecting the state that the GUI is querying.
*/
- (unsigned int) getLastErrorID
{
return lastQueryErrorId;
}
/*
* Returns the number of affected rows by the last query.
* This is cached within the object to allow helper queries to be performed
* without affecting the state that the GUI is querying.
*/
- (my_ulonglong) affectedRows
{
return lastQueryAffectedRows;
}
/*
* Retrieve the max_allowed_packet size from the server; returns
* false if the query fails.
*/
- (BOOL) fetchMaxAllowedPacket
{
char *queryString;
if ([self serverMajorVersion] == 3) queryString = "SHOW VARIABLES LIKE 'max_allowed_packet'";
else queryString = "SELECT @@global.max_allowed_packet";
if (0 == mysql_query(mConnection, queryString)) {
if (mysql_field_count(mConnection) != 0) {
CMMCPResult *r = [[CMMCPResult alloc] initWithMySQLPtr:mConnection encoding:mEncoding timeZone:mTimeZone];
NSArray *a = [r fetchRowAsArray];
[r autorelease];
if([a count]) {
maxAllowedPacketSize = [[a objectAtIndex:([self serverMajorVersion] == 3)?1:0] intValue];
return true;
}
}
}
return false;
}
/*
* Retrieves max_allowed_packet size set as global variable.
* It returns -1 if it fails.
*/
- (int) getMaxAllowedPacket
{
return maxAllowedPacketSize;
}
/*
* It sets max_allowed_packet size to newSize and it returns
* max_allowed_packet after setting it to newSize for cross-checking
* if the maximal size was reached (e.g. set it to 4GB it'll return 1GB up to now).
* If something failed it return -1;
*/
- (int) setMaxAllowedPacketTo:(int)newSize resetSize:(BOOL)reset
{
if(![self isMaxAllowedPacketEditable] || newSize < 1024) return maxAllowedPacketSize;
mysql_query(mConnection, [[NSString stringWithFormat:@"SET GLOBAL max_allowed_packet = %d", newSize] UTF8String]);
// Inform the user via a log entry about that change according to reset value
if(delegate && [delegate respondsToSelector:@selector(queryGaveError:)])
if(reset)
[delegate queryGaveError:[NSString stringWithFormat:@"max_allowed_packet was reset to %d for new session", newSize]];
else
[delegate queryGaveError:[NSString stringWithFormat:@"Query too large; max_allowed_packet temporarily set to %d for the current session to allow query to succeed", newSize]];
return maxAllowedPacketSize;
}
/*
* It returns whether max_allowed_packet is setable for the user.
*/
- (BOOL) isMaxAllowedPacketEditable
{
return(!mysql_query(mConnection, "SET GLOBAL max_allowed_packet = @@global.max_allowed_packet"));
}
- (void) dealloc
{
if (lastQueryErrorMessage) [lastQueryErrorMessage release];
if (connectionHost) [connectionHost release];
if (connectionLogin) [connectionLogin release];
if (connectionSocket) [connectionSocket release];
if (connectionPassword) [connectionPassword release];
if (connectionKeychainName) [connectionKeychainName release];
if (connectionKeychainAccount) [connectionKeychainAccount release];
if (lastKeepAliveSuccess) [lastKeepAliveSuccess release];
[super dealloc];
}
@end