diff --git a/README.md b/README.md index 4a56782..0098fa3 100644 --- a/README.md +++ b/README.md @@ -156,3 +156,7 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +## What's new +1.2.3 - fixed ios socket closing crashes +1.5.0 - added ios open and write timeouts, changed js errors format \ No newline at end of file diff --git a/package.json b/package.json index 694dcdd..fb7bcd6 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,8 @@ { - "name": "SocketsForCordova", - "version": "1.2.0", + "name": "cordova-plugin-socket-tcp", + "version": "1.5.0", "description": "This Cordova plugin provides JavaScript API, that allows you to communicate with server through TCP protocol. Currently we support these platforms: iOS, Android, WP8.", "cordova": { - "id": "cz.blocshop.socketsforcordova", "platforms": [ "ios", "android", diff --git a/plugin.xml b/plugin.xml index 53daabf..ab4e9d0 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,6 +1,6 @@ - + SocketsForCordova This Cordova plugin provides JavaScript API, that allows you to communicate with server through TCP protocol. diff --git a/socket.js b/socket.js index c5215c9..5d6a5c2 100644 --- a/socket.js +++ b/socket.js @@ -1,213 +1,221 @@ -/** - * Copyright (c) 2015, Blocshop s.r.o. - * All rights reserved. - * - * Redistribution and use in source and binary forms are permitted - * provided that the above copyright notice and this paragraph are - * duplicated in all such forms and that any documentation, - * advertising materials, and other materials related to such - * distribution and use acknowledge that the software was developed - * by the Blocshop s.r.o.. The name of the - * Blocshop s.r.o. may not be used to endorse or promote products derived - * from this software without specific prior written permission. - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. - */ +cordova.define("cordova-plugin-socket-tcp.Socket", function(require, exports, module) { + /** + * Copyright (c) 2015, Blocshop s.r.o. + * All rights reserved. + * + * Redistribution and use in source and binary forms are permitted + * provided that the above copyright notice and this paragraph are + * duplicated in all such forms and that any documentation, + * advertising materials, and other materials related to such + * distribution and use acknowledge that the software was developed + * by the Blocshop s.r.o.. The name of the + * Blocshop s.r.o. may not be used to endorse or promote products derived + * from this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + */ -var exec = require('cordova/exec'); + var exec = require('cordova/exec'); -var SOCKET_EVENT = "SOCKET_EVENT"; -var CORDOVA_SERVICE_NAME = "SocketsForCordova"; + var SOCKET_EVENT = "SOCKET_EVENT"; + var CORDOVA_SERVICE_NAME = "SocketsForCordova"; -Socket.State = {}; -Socket.State[Socket.State.CLOSED = 0] = "CLOSED"; -Socket.State[Socket.State.OPENING = 1] = "OPENING"; -Socket.State[Socket.State.OPENED = 2] = "OPENED"; -Socket.State[Socket.State.CLOSING = 3] = "CLOSING"; + Socket.State = {}; + Socket.State[Socket.State.CLOSED = 0] = "CLOSED"; + Socket.State[Socket.State.OPENING = 1] = "OPENING"; + Socket.State[Socket.State.OPENED = 2] = "OPENED"; + Socket.State[Socket.State.CLOSING = 3] = "CLOSING"; -function Socket() { - this._state = Socket.State.CLOSED; - this.onData = null; - this.onClose = null; - this.onError = null; - this.socketKey = guid(); -} + Socket.ErrorType = {}; + Socket.ErrorType[Socket.ErrorType.GENERAL = 0] = "general"; + Socket.ErrorType[Socket.ErrorType.OPEN_TIMEOUT = 1] = "openTimeout"; + Socket.ErrorType[Socket.ErrorType.WRITE_TIMEOUT = 2] = "writeTimeout"; -Socket.prototype.open = function (host, port, success, error) { - - success = success || function() { }; - error = error || function() { }; - - if (!this._ensureState(Socket.State.CLOSED, error)) { - return; + function Socket() { + this._state = Socket.State.CLOSED; + this.onData = null; + this.onClose = null; + this.onError = null; + this.socketKey = guid(); } - var _that = this; + Socket.prototype.open = function (host, port, success, error) { - function socketEventHandler(event) { + success = success || function() { }; + error = error || function() { }; - var payload = event.payload; - - if (payload.socketKey !== _that.socketKey) { + if (!this._ensureState(Socket.State.CLOSED, error)) { return; } - switch (payload.type) { - case "Close": - _that._state = Socket.State.CLOSED; - window.document.removeEventListener(SOCKET_EVENT, socketEventHandler); - _that.onClose(payload.hasError); - break; - case "DataReceived": - _that.onData(new Uint8Array(payload.data)); - break; - case "Error": - _that.onError(payload.errorMessage); - break; - default: - console.error("SocketsForCordova: Unknown event type " + payload.type + ", socket key: " + payload.socketKey); - break; + var _that = this; + + function socketEventHandler(event) { + + var payload = event.payload; + + if (payload.socketKey !== _that.socketKey) { + return; + } + + switch (payload.type) { + case "Close": + _that._state = Socket.State.CLOSED; + window.document.removeEventListener(SOCKET_EVENT, socketEventHandler); + _that.onClose(payload.hasError); + break; + case "DataReceived": + _that.onData(new Uint8Array(payload.data)); + break; + case "Error": + _that.onError(payload); + break; + default: + console.error("SocketsForCordova: Unknown event type " + payload.type + ", socket key: " + payload.socketKey); + break; + } } - } - _that._state = Socket.State.OPENING; + _that._state = Socket.State.OPENING; - exec( - function () { - _that._state = Socket.State.OPENED; - window.document.addEventListener(SOCKET_EVENT, socketEventHandler); - success(); - }, - function(errorMessage) { - _that._state = Socket.State.CLOSED; - error(errorMessage); - }, - CORDOVA_SERVICE_NAME, - "open", - [ this.socketKey, host, port ]); -}; - -Socket.prototype.write = function (data, success, error) { - - success = success || function() { }; - error = error || function() { }; - - if (!this._ensureState(Socket.State.OPENED, error)) { - return; - } - - var dataToWrite = data instanceof Uint8Array - ? Socket._copyToArray(data) - : data; - - exec( - success, - error, - CORDOVA_SERVICE_NAME, - "write", - [ this.socketKey, dataToWrite ]); -}; - -Socket.prototype.shutdownWrite = function (success, error) { - - success = success || function() { }; - error = error || function() { }; - - if (!this._ensureState(Socket.State.OPENED, error)) { - return; - } - - exec( - success, - error, - CORDOVA_SERVICE_NAME, - "shutdownWrite", - [ this.socketKey ]); -}; - -Socket.prototype.close = function (success, error) { - - success = success || function() { }; - error = error || function() { }; - - if (!this._ensureState(Socket.State.OPENED, error)) { - return; - } - - this._state = Socket.State.CLOSING; - - exec( - success, - error, - CORDOVA_SERVICE_NAME, - "close", - [ this.socketKey ]); -}; - -Object.defineProperty(Socket.prototype, "state", { - get: function () { - return this._state; - }, - enumerable: true, - configurable: true -}); - -Socket.prototype._ensureState = function(requiredState, errorCallback) { - var state = this._state; - if (state != requiredState) { - window.setTimeout(function() { - errorCallback("Invalid operation for this socket state: " + Socket.State[state]); - }); - return false; - } - else { - return true; - } -}; - -Socket.dispatchEvent = function (event) { - var eventReceive = document.createEvent('Events'); - eventReceive.initEvent(SOCKET_EVENT, true, true); - eventReceive.payload = event; - - document.dispatchEvent(eventReceive); -}; - -Socket._copyToArray = function (array) { - var outputArray = new Array(array.length); - for (var i = 0; i < array.length; i++) { - outputArray[i] = array[i]; - } - return outputArray; -}; - -var guid = (function () { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - - return function () { - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + - s4() + '-' + s4() + s4() + s4(); - }; -})(); - -// Register event dispatcher for Windows Phone -if (navigator.userAgent.match(/iemobile/i)) { - window.document.addEventListener("deviceready", function () { exec( - Socket.dispatchEvent, - function (errorMessage) { - console.error("SocketsForCordova: Cannot register WP event dispatcher, Error: " + errorMessage); + function () { + _that._state = Socket.State.OPENED; + window.document.addEventListener(SOCKET_EVENT, socketEventHandler); + success(); + }, + function(errorMessage) { + _that._state = Socket.State.CLOSED; + error(errorMessage); }, CORDOVA_SERVICE_NAME, - "registerWPEventDispatcher", - [ ]); - }); -} + "open", + [ this.socketKey, host, port ]); + }; -module.exports = Socket; + Socket.prototype.write = function (data, success, error) { + + success = success || function() { }; + error = error || function() { }; + + if (!this._ensureState(Socket.State.OPENED, error)) { + return; + } + + var dataToWrite = data instanceof Uint8Array + ? Socket._copyToArray(data) + : data; + + exec( + success, + error, + CORDOVA_SERVICE_NAME, + "write", + [ this.socketKey, dataToWrite ]); + }; + + Socket.prototype.shutdownWrite = function (success, error) { + + success = success || function() { }; + error = error || function() { }; + + if (!this._ensureState(Socket.State.OPENED, error)) { + return; + } + + exec( + success, + error, + CORDOVA_SERVICE_NAME, + "shutdownWrite", + [ this.socketKey ]); + }; + + Socket.prototype.close = function (success, error) { + + success = success || function() { }; + error = error || function() { }; + + if (!this._ensureState(Socket.State.OPENED, error)) { + return; + } + + this._state = Socket.State.CLOSING; + + exec( + success, + error, + CORDOVA_SERVICE_NAME, + "close", + [ this.socketKey ]); + }; + + Object.defineProperty(Socket.prototype, "state", { + get: function () { + return this._state; + }, + enumerable: true, + configurable: true + }); + + Socket.prototype._ensureState = function(requiredState, errorCallback) { + var state = this._state; + if (state != requiredState) { + window.setTimeout(function() { + errorCallback("Invalid operation for this socket state: " + Socket.State[state]); + }); + return false; + } + else { + return true; + } + }; + + Socket.dispatchEvent = function (event) { + var eventReceive = document.createEvent('Events'); + eventReceive.initEvent(SOCKET_EVENT, true, true); + eventReceive.payload = event; + + document.dispatchEvent(eventReceive); + }; + + Socket._copyToArray = function (array) { + var outputArray = new Array(array.length); + for (var i = 0; i < array.length; i++) { + outputArray[i] = array[i]; + } + return outputArray; + }; + + var guid = (function () { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + + return function () { + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); + }; + })(); + +// Register event dispatcher for Windows Phone + if (navigator.userAgent.match(/iemobile/i)) { + window.document.addEventListener("deviceready", function () { + exec( + Socket.dispatchEvent, + function (errorMessage) { + console.error("SocketsForCordova: Cannot register WP event dispatcher, Error: " + errorMessage); + }, + CORDOVA_SERVICE_NAME, + "registerWPEventDispatcher", + [ ]); + }); + } + + module.exports = Socket; + +}); \ No newline at end of file diff --git a/src/ios/SocketsForCordova/Classes/SocketAdapter.h b/src/ios/SocketsForCordova/Classes/SocketAdapter.h index 7b0c6e0..c609205 100644 --- a/src/ios/SocketsForCordova/Classes/SocketAdapter.h +++ b/src/ios/SocketsForCordova/Classes/SocketAdapter.h @@ -24,6 +24,9 @@ NSInputStream *inputStream1; NSOutputStream *outputStream1; + + NSTimer *openTimer; + NSTimer *writeTimer; } - (void)open:(NSString *)host port:(NSNumber*)port; @@ -36,6 +39,6 @@ @property (copy) void (^openErrorEventHandler)(NSString*); @property (copy) void (^dataConsumer)(NSArray*); @property (copy) void (^closeEventHandler)(BOOL); -@property (copy) void (^errorEventHandler)(NSString*); +@property (copy) void (^errorEventHandler)(NSString*, NSString *); @end \ No newline at end of file diff --git a/src/ios/SocketsForCordova/Classes/SocketAdapter.m b/src/ios/SocketsForCordova/Classes/SocketAdapter.m index 88522fe..8587546 100644 --- a/src/ios/SocketsForCordova/Classes/SocketAdapter.m +++ b/src/ios/SocketsForCordova/Classes/SocketAdapter.m @@ -27,19 +27,24 @@ CFWriteStreamRef writeStream; NSInputStream *inputStream; NSOutputStream *outputStream; +NSTimer *openTimer; +NSTimer *writeTimer; + BOOL wasOpenned = FALSE; int const WRITE_BUFFER_SIZE = 10 * 1024; +int openTimeoutSeconds = 5.0; +int writeTimeoutSeconds = 5.0; + @implementation SocketAdapter - (void)open:(NSString *)host port:(NSNumber*)port { - + CFReadStreamRef readStream2; CFWriteStreamRef writeStream2; - - NSLog(@"Setting up connection to %@ : %@", host, [port stringValue]); + NSLog(@"[NATIVE] Setting up connection to %@ : %@", host, [port stringValue]); if (![self isIp:host]) { host = [self resolveIp:host]; @@ -51,7 +56,7 @@ int const WRITE_BUFFER_SIZE = 10 * 1024; CFWriteStreamSetProperty(writeStream2, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); if(!CFWriteStreamOpen(writeStream2) || !CFReadStreamOpen(readStream2)) { - NSLog(@"Error, streams not open"); + NSLog(@"[NATIVE] Error, streams not open"); @throw [NSException exceptionWithName:@"SocketException" reason:@"Cannot open streams." userInfo:nil]; } @@ -60,12 +65,28 @@ int const WRITE_BUFFER_SIZE = 10 * 1024; [inputStream1 setDelegate:self]; [inputStream1 open]; + NSTimer *timer = [NSTimer timerWithTimeInterval:openTimeoutSeconds target:self selector:@selector(onOpenTimeout:) userInfo:nil repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; + openTimer = timer; + outputStream1 = (__bridge NSOutputStream *)writeStream2; [outputStream1 open]; [self performSelectorOnMainThread:@selector(runReadLoop) withObject:nil waitUntilDone:NO]; } +-(void)onOpenTimeout:(NSTimer *)timer { + NSLog(@"[NATIVE] Open timeout: %d", openTimeoutSeconds); + self.errorEventHandler(@"openTimeout", @"timeout"); + openTimer = nil; +} + +-(void)onWriteTimeout:(NSTimer *)timer { + NSLog(@"[NATIVE] Write timeout: %d", writeTimeoutSeconds); + self.errorEventHandler(@"writeTimeout", @"timeout"); + writeTimer = nil; +} + -(BOOL)isIp:(NSString*) host { const char *utf8 = [host UTF8String]; @@ -82,7 +103,7 @@ int const WRITE_BUFFER_SIZE = 10 * 1024; -(NSString*)resolveIp:(NSString*) host { - NSLog(@"Resolving host: %@", host); + NSLog(@"[NATIVE] Resolving host: %@", host); const char *buff = [host cStringUsingEncoding:NSUTF8StringEncoding]; struct hostent *host_entry = gethostbyname(buff); @@ -94,7 +115,7 @@ int const WRITE_BUFFER_SIZE = 10 * 1024; char *hostCstring = inet_ntoa(*((struct in_addr *)host_entry->h_addr_list[0])); host = [NSString stringWithUTF8String:hostCstring]; - NSLog(@"Resolved ip: %@", host); + NSLog(@"[NATIVE] Resolved ip: %@", host); return host; } @@ -104,7 +125,7 @@ int const WRITE_BUFFER_SIZE = 10 * 1024; } - (void)shutdownWrite { - NSLog(@"Shuting down write on socket."); + NSLog(@"[NATIVE] Shuting down write on socket."); [self closeOutputStream]; @@ -143,10 +164,21 @@ int const WRITE_BUFFER_SIZE = 10 * 1024; case NSStreamEventOpenCompleted: { self.openEventHandler(); wasOpenned = TRUE; + if(openTimer != nil){ + NSLog(@"[NATIVE] openTimer invalidate on open event"); + [openTimer invalidate]; + openTimer = nil; + } break; } case NSStreamEventHasBytesAvailable: { if(stream == inputStream1) { + if(writeTimer != nil){ + NSLog(@"[NATIVE] writeTimer invalidate on has bytes event"); + [writeTimer invalidate]; + writeTimer = nil; + } + uint8_t buf[65535]; long len = [inputStream1 read:buf maxLength:65535]; @@ -172,14 +204,14 @@ int const WRITE_BUFFER_SIZE = 10 * 1024; } case NSStreamEventErrorOccurred: { - NSLog(@"Stream event error: %@", [[stream streamError] localizedDescription]); + NSLog(@"[NATIVE] Stream event error: %@", [[stream streamError] localizedDescription]); if (wasOpenned) { - self.errorEventHandler([[stream streamError] localizedDescription]); + self.errorEventHandler([[stream streamError] localizedDescription], @"general"); self.closeEventHandler(TRUE); } else { - self.errorEventHandler([[stream streamError] localizedDescription]); + self.errorEventHandler([[stream streamError] localizedDescription], @"general"); self.openErrorEventHandler([[stream streamError] localizedDescription]); } //[self closeStreams]; @@ -198,6 +230,11 @@ int const WRITE_BUFFER_SIZE = 10 * 1024; [self writeSubarray:dataArray offset:i * WRITE_BUFFER_SIZE length:WRITE_BUFFER_SIZE]; } int lastBatchPosition = (numberOfBatches - 1) * WRITE_BUFFER_SIZE; + + NSTimer *timer = [NSTimer timerWithTimeInterval:writeTimeoutSeconds target:self selector:@selector(onWriteTimeout:) userInfo:nil repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; + writeTimer = timer; + [self writeSubarray:dataArray offset:lastBatchPosition length:(dataArray.count - lastBatchPosition)]; } @@ -224,6 +261,18 @@ int const WRITE_BUFFER_SIZE = 10 * 1024; - (void)closeStreams { [self closeOutputStream]; [self closeInputStream]; + + if(writeTimer != nil){ + [writeTimer invalidate]; + writeTimer = nil; + NSLog(@"[NATIVE] writeTimer invalidate on close"); + } + + if(openTimer != nil){ + [openTimer invalidate]; + openTimer = nil; + NSLog(@"[NATIVE] openTimer invalidate on close"); + } } @end \ No newline at end of file diff --git a/src/ios/SocketsForCordova/Classes/SocketPlugin.m b/src/ios/SocketsForCordova/Classes/SocketPlugin.m index f09184b..b1a32ac 100644 --- a/src/ios/SocketsForCordova/Classes/SocketPlugin.m +++ b/src/ios/SocketsForCordova/Classes/SocketPlugin.m @@ -47,9 +47,10 @@ socketAdapter = nil; }; - socketAdapter.errorEventHandler = ^ void (NSString *error){ + socketAdapter.errorEventHandler = ^ void (NSString *error, NSString *errorType){ NSMutableDictionary *errorDictionaryData = [[NSMutableDictionary alloc] init]; [errorDictionaryData setObject:@"Error" forKey:@"type"]; + [errorDictionaryData setObject:errorType forKey:@"errorType"]; [errorDictionaryData setObject:error forKey:@"errorMessage"]; [errorDictionaryData setObject:socketKey forKey:@"socketKey"]; @@ -97,7 +98,11 @@ [self.commandDelegate runInBackground:^{ @try { - [socket write:data]; + if (socket != nil) { + [socket write:data]; + }else{ + NSLog(@"[NATIVE] Write: socket is nil. SocketKey: %@", socketKey); + } [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; @@ -118,7 +123,12 @@ [self.commandDelegate runInBackground:^{ @try { - [socket shutdownWrite]; + if (socket != nil) { + [socket shutdownWrite]; + }else{ + NSLog(@"[NATIVE] ShutdownWrite: socket is nil. SocketKey: %@", socketKey); + } + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; @@ -139,7 +149,12 @@ [self.commandDelegate runInBackground:^{ @try { - [socket close]; + if (socket != nil) { + [socket close]; + }else{ + NSLog(@"[NATIVE] Close: socket is nil. SocketKey: %@", socketKey); + } + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK] callbackId:command.callbackId]; @@ -158,15 +173,16 @@ - (SocketAdapter*) getSocketAdapter: (NSString*) socketKey { SocketAdapter* socketAdapter = [self->socketAdapters objectForKey:socketKey]; if (socketAdapter == nil) { - NSString *exceptionReason = [NSString stringWithFormat:@"Cannot find socketKey: %@. Connection is probably closed.", socketKey]; + NSLog(@"[NATIVE] Cannot find socketKey: %@. Connection is probably closed.", socketKey); + //NSString *exceptionReason = [NSString stringWithFormat:@"Cannot find socketKey: %@. Connection is probably closed.", socketKey]; - @throw [NSException exceptionWithName:@"IllegalArgumentException" reason:exceptionReason userInfo:nil]; + //@throw [NSException exceptionWithName:@"IllegalArgumentException" reason:exceptionReason userInfo:nil]; } return socketAdapter; } - (void) removeSocketAdapter: (NSString*) socketKey { - NSLog(@"Removing socket adapter from storage."); + NSLog(@"[NATIVE] Removing socket adapter from storage."); [self->socketAdapters removeObjectForKey:socketKey]; } @@ -178,7 +194,7 @@ - (void) dispatchEventWithDictionary: (NSDictionary*) dictionary { NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:nil]; NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - + [self dispatchEvent:jsonString]; }