diff --git a/CMakeLists.txt b/CMakeLists.txt index e01986b355..e0a03036ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ endif (NOT WIN32) # targets on all platforms add_subdirectory(assignment-client) +add_subdirectory(data-server) add_subdirectory(domain-server) add_subdirectory(interface) add_subdirectory(pairing-server) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index ca89d3998f..d27845f337 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -56,7 +56,7 @@ void Agent::run() { // other node types, but for them to get access to those node types, we have to add them here. It seems like // NodeList should support adding types of interest const NODE_TYPE AGENT_NODE_TYPES_OF_INTEREST[] = { NODE_TYPE_VOXEL_SERVER, NODE_TYPE_PARTICLE_SERVER, - NODE_TYPE_AUDIO_MIXER }; + NODE_TYPE_AUDIO_MIXER, NODE_TYPE_AVATAR_MIXER }; nodeList->setNodeTypesOfInterest(AGENT_NODE_TYPES_OF_INTEREST, sizeof(AGENT_NODE_TYPES_OF_INTEREST)); @@ -93,6 +93,15 @@ void Agent::run() { QTimer* pingNodesTimer = new QTimer(this); connect(pingNodesTimer, SIGNAL(timeout()), nodeList, SLOT(pingInactiveNodes())); pingNodesTimer->start(PING_INACTIVE_NODE_INTERVAL_USECS / 1000); + + // setup an Avatar for the script to use + AvatarData scriptedAvatar; + + // match the scripted avatar's UUID to the DataServerScriptingInterface UUID + scriptedAvatar.setUUID(_scriptEngine.getDataServerScriptingInterface().getUUID()); + + // give this AvatarData object to the script engine + _scriptEngine.setAvatarData(&scriptedAvatar); _scriptEngine.setScriptContents(scriptContents); _scriptEngine.run(); diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index e5e3ea2fcc..56464d2415 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -137,19 +137,6 @@ void AvatarMixer::processDatagram(const QByteArray& dataByteArray, const HifiSoc } } case PACKET_TYPE_KILL_NODE: - case PACKET_TYPE_AVATAR_URLS: { - QUuid nodeUUID = QUuid::fromRfc4122(dataByteArray.mid(numBytesForPacketHeader((unsigned char*) dataByteArray.data()), - NUM_BYTES_RFC4122_UUID)); - // let everyone else know about the update - foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { - if (node->getActiveSocket() && node->getUUID() != nodeUUID) { - nodeList->getNodeSocket().writeDatagram(dataByteArray, - node->getActiveSocket()->getAddress(), - node->getActiveSocket()->getPort()); - } - } - // let node kills fall through to default behavior - } default: // hand this off to the NodeList nodeList->processNodeData(senderSockAddr, (unsigned char*) dataByteArray.data(), dataByteArray.size()); diff --git a/data-server/CMakeLists.txt b/data-server/CMakeLists.txt new file mode 100755 index 0000000000..65e3e4a489 --- /dev/null +++ b/data-server/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 2.8) + +set(ROOT_DIR ..) +set(MACRO_DIR ${ROOT_DIR}/cmake/macros) + +set(TARGET_NAME data-server) + +find_package(Qt5Network REQUIRED) + +include(${MACRO_DIR}/SetupHifiProject.cmake) +setup_hifi_project(${TARGET_NAME} TRUE) + +qt5_use_modules(${TARGET_NAME} Network) + +# link the shared hifi library +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) + +# add hiredis as a library +FILE(GLOB HIREDIS_SRCS external/hiredis/*.c) +add_library(hiredis ${HIREDIS_SRCS}) +include_directories(external/hiredis/) +target_link_libraries(${TARGET_NAME} hiredis) \ No newline at end of file diff --git a/data-server/external/hiredis/async.c b/data-server/external/hiredis/async.c new file mode 100755 index 0000000000..65327120fe --- /dev/null +++ b/data-server/external/hiredis/async.c @@ -0,0 +1,648 @@ +/* + * Copyright (c) 2009-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; 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. + */ + +#include "fmacros.h" +#include +#include +#include +#include +#include +#include +#include "async.h" +#include "net.h" +#include "dict.c" +#include "sds.h" + +#define _EL_ADD_READ(ctx) do { \ + if ((ctx)->ev.addRead) (ctx)->ev.addRead((ctx)->ev.data); \ + } while(0) +#define _EL_DEL_READ(ctx) do { \ + if ((ctx)->ev.delRead) (ctx)->ev.delRead((ctx)->ev.data); \ + } while(0) +#define _EL_ADD_WRITE(ctx) do { \ + if ((ctx)->ev.addWrite) (ctx)->ev.addWrite((ctx)->ev.data); \ + } while(0) +#define _EL_DEL_WRITE(ctx) do { \ + if ((ctx)->ev.delWrite) (ctx)->ev.delWrite((ctx)->ev.data); \ + } while(0) +#define _EL_CLEANUP(ctx) do { \ + if ((ctx)->ev.cleanup) (ctx)->ev.cleanup((ctx)->ev.data); \ + } while(0); + +/* Forward declaration of function in hiredis.c */ +void __redisAppendCommand(redisContext *c, char *cmd, size_t len); + +/* Functions managing dictionary of callbacks for pub/sub. */ +static unsigned int callbackHash(const void *key) { + return dictGenHashFunction((const unsigned char *)key, + sdslen((const sds)key)); +} + +static void *callbackValDup(void *privdata, const void *src) { + ((void) privdata); + redisCallback *dup = malloc(sizeof(*dup)); + memcpy(dup,src,sizeof(*dup)); + return dup; +} + +static int callbackKeyCompare(void *privdata, const void *key1, const void *key2) { + int l1, l2; + ((void) privdata); + + l1 = sdslen((const sds)key1); + l2 = sdslen((const sds)key2); + if (l1 != l2) return 0; + return memcmp(key1,key2,l1) == 0; +} + +static void callbackKeyDestructor(void *privdata, void *key) { + ((void) privdata); + sdsfree((sds)key); +} + +static void callbackValDestructor(void *privdata, void *val) { + ((void) privdata); + free(val); +} + +static dictType callbackDict = { + callbackHash, + NULL, + callbackValDup, + callbackKeyCompare, + callbackKeyDestructor, + callbackValDestructor +}; + +static redisAsyncContext *redisAsyncInitialize(redisContext *c) { + redisAsyncContext *ac; + + ac = realloc(c,sizeof(redisAsyncContext)); + if (ac == NULL) + return NULL; + + c = &(ac->c); + + /* The regular connect functions will always set the flag REDIS_CONNECTED. + * For the async API, we want to wait until the first write event is + * received up before setting this flag, so reset it here. */ + c->flags &= ~REDIS_CONNECTED; + + ac->err = 0; + ac->errstr = NULL; + ac->data = NULL; + + ac->ev.data = NULL; + ac->ev.addRead = NULL; + ac->ev.delRead = NULL; + ac->ev.addWrite = NULL; + ac->ev.delWrite = NULL; + ac->ev.cleanup = NULL; + + ac->onConnect = NULL; + ac->onDisconnect = NULL; + + ac->replies.head = NULL; + ac->replies.tail = NULL; + ac->sub.invalid.head = NULL; + ac->sub.invalid.tail = NULL; + ac->sub.channels = dictCreate(&callbackDict,NULL); + ac->sub.patterns = dictCreate(&callbackDict,NULL); + return ac; +} + +/* We want the error field to be accessible directly instead of requiring + * an indirection to the redisContext struct. */ +static void __redisAsyncCopyError(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + ac->err = c->err; + ac->errstr = c->errstr; +} + +redisAsyncContext *redisAsyncConnect(const char *ip, int port) { + redisContext *c; + redisAsyncContext *ac; + + c = redisConnectNonBlock(ip,port); + if (c == NULL) + return NULL; + + ac = redisAsyncInitialize(c); + if (ac == NULL) { + redisFree(c); + return NULL; + } + + __redisAsyncCopyError(ac); + return ac; +} + +redisAsyncContext *redisAsyncConnectUnix(const char *path) { + redisContext *c; + redisAsyncContext *ac; + + c = redisConnectUnixNonBlock(path); + if (c == NULL) + return NULL; + + ac = redisAsyncInitialize(c); + __redisAsyncCopyError(ac); + return ac; +} + +int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn) { + if (ac->onConnect == NULL) { + ac->onConnect = fn; + + /* The common way to detect an established connection is to wait for + * the first write event to be fired. This assumes the related event + * library functions are already set. */ + _EL_ADD_WRITE(ac); + return REDIS_OK; + } + return REDIS_ERR; +} + +int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn) { + if (ac->onDisconnect == NULL) { + ac->onDisconnect = fn; + return REDIS_OK; + } + return REDIS_ERR; +} + +/* Helper functions to push/shift callbacks */ +static int __redisPushCallback(redisCallbackList *list, redisCallback *source) { + redisCallback *cb; + + /* Copy callback from stack to heap */ + cb = malloc(sizeof(*cb)); + if (cb == NULL) + return REDIS_ERR_OOM; + + if (source != NULL) { + memcpy(cb,source,sizeof(*cb)); + cb->next = NULL; + } + + /* Store callback in list */ + if (list->head == NULL) + list->head = cb; + if (list->tail != NULL) + list->tail->next = cb; + list->tail = cb; + return REDIS_OK; +} + +static int __redisShiftCallback(redisCallbackList *list, redisCallback *target) { + redisCallback *cb = list->head; + if (cb != NULL) { + list->head = cb->next; + if (cb == list->tail) + list->tail = NULL; + + /* Copy callback from heap to stack */ + if (target != NULL) + memcpy(target,cb,sizeof(*cb)); + free(cb); + return REDIS_OK; + } + return REDIS_ERR; +} + +static void __redisRunCallback(redisAsyncContext *ac, redisCallback *cb, redisReply *reply) { + redisContext *c = &(ac->c); + if (cb->fn != NULL) { + c->flags |= REDIS_IN_CALLBACK; + cb->fn(ac,reply,cb->privdata); + c->flags &= ~REDIS_IN_CALLBACK; + } +} + +/* Helper function to free the context. */ +static void __redisAsyncFree(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + redisCallback cb; + dictIterator *it; + dictEntry *de; + + /* Execute pending callbacks with NULL reply. */ + while (__redisShiftCallback(&ac->replies,&cb) == REDIS_OK) + __redisRunCallback(ac,&cb,NULL); + + /* Execute callbacks for invalid commands */ + while (__redisShiftCallback(&ac->sub.invalid,&cb) == REDIS_OK) + __redisRunCallback(ac,&cb,NULL); + + /* Run subscription callbacks callbacks with NULL reply */ + it = dictGetIterator(ac->sub.channels); + while ((de = dictNext(it)) != NULL) + __redisRunCallback(ac,dictGetEntryVal(de),NULL); + dictReleaseIterator(it); + dictRelease(ac->sub.channels); + + it = dictGetIterator(ac->sub.patterns); + while ((de = dictNext(it)) != NULL) + __redisRunCallback(ac,dictGetEntryVal(de),NULL); + dictReleaseIterator(it); + dictRelease(ac->sub.patterns); + + /* Signal event lib to clean up */ + _EL_CLEANUP(ac); + + /* Execute disconnect callback. When redisAsyncFree() initiated destroying + * this context, the status will always be REDIS_OK. */ + if (ac->onDisconnect && (c->flags & REDIS_CONNECTED)) { + if (c->flags & REDIS_FREEING) { + ac->onDisconnect(ac,REDIS_OK); + } else { + ac->onDisconnect(ac,(ac->err == 0) ? REDIS_OK : REDIS_ERR); + } + } + + /* Cleanup self */ + redisFree(c); +} + +/* Free the async context. When this function is called from a callback, + * control needs to be returned to redisProcessCallbacks() before actual + * free'ing. To do so, a flag is set on the context which is picked up by + * redisProcessCallbacks(). Otherwise, the context is immediately free'd. */ +void redisAsyncFree(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + c->flags |= REDIS_FREEING; + if (!(c->flags & REDIS_IN_CALLBACK)) + __redisAsyncFree(ac); +} + +/* Helper function to make the disconnect happen and clean up. */ +static void __redisAsyncDisconnect(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + + /* Make sure error is accessible if there is any */ + __redisAsyncCopyError(ac); + + if (ac->err == 0) { + /* For clean disconnects, there should be no pending callbacks. */ + assert(__redisShiftCallback(&ac->replies,NULL) == REDIS_ERR); + } else { + /* Disconnection is caused by an error, make sure that pending + * callbacks cannot call new commands. */ + c->flags |= REDIS_DISCONNECTING; + } + + /* For non-clean disconnects, __redisAsyncFree() will execute pending + * callbacks with a NULL-reply. */ + __redisAsyncFree(ac); +} + +/* Tries to do a clean disconnect from Redis, meaning it stops new commands + * from being issued, but tries to flush the output buffer and execute + * callbacks for all remaining replies. When this function is called from a + * callback, there might be more replies and we can safely defer disconnecting + * to redisProcessCallbacks(). Otherwise, we can only disconnect immediately + * when there are no pending callbacks. */ +void redisAsyncDisconnect(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + c->flags |= REDIS_DISCONNECTING; + if (!(c->flags & REDIS_IN_CALLBACK) && ac->replies.head == NULL) + __redisAsyncDisconnect(ac); +} + +static int __redisGetSubscribeCallback(redisAsyncContext *ac, redisReply *reply, redisCallback *dstcb) { + redisContext *c = &(ac->c); + dict *callbacks; + dictEntry *de; + int pvariant; + char *stype; + sds sname; + + /* Custom reply functions are not supported for pub/sub. This will fail + * very hard when they are used... */ + if (reply->type == REDIS_REPLY_ARRAY) { + assert(reply->elements >= 2); + assert(reply->element[0]->type == REDIS_REPLY_STRING); + stype = reply->element[0]->str; + pvariant = (tolower(stype[0]) == 'p') ? 1 : 0; + + if (pvariant) + callbacks = ac->sub.patterns; + else + callbacks = ac->sub.channels; + + /* Locate the right callback */ + assert(reply->element[1]->type == REDIS_REPLY_STRING); + sname = sdsnewlen(reply->element[1]->str,reply->element[1]->len); + de = dictFind(callbacks,sname); + if (de != NULL) { + memcpy(dstcb,dictGetEntryVal(de),sizeof(*dstcb)); + + /* If this is an unsubscribe message, remove it. */ + if (strcasecmp(stype+pvariant,"unsubscribe") == 0) { + dictDelete(callbacks,sname); + + /* If this was the last unsubscribe message, revert to + * non-subscribe mode. */ + assert(reply->element[2]->type == REDIS_REPLY_INTEGER); + if (reply->element[2]->integer == 0) + c->flags &= ~REDIS_SUBSCRIBED; + } + } + sdsfree(sname); + } else { + /* Shift callback for invalid commands. */ + __redisShiftCallback(&ac->sub.invalid,dstcb); + } + return REDIS_OK; +} + +void redisProcessCallbacks(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + redisCallback cb; + void *reply = NULL; + int status; + + while((status = redisGetReply(c,&reply)) == REDIS_OK) { + if (reply == NULL) { + /* When the connection is being disconnected and there are + * no more replies, this is the cue to really disconnect. */ + if (c->flags & REDIS_DISCONNECTING && sdslen(c->obuf) == 0) { + __redisAsyncDisconnect(ac); + return; + } + + /* If monitor mode, repush callback */ + if(c->flags & REDIS_MONITORING) { + __redisPushCallback(&ac->replies,&cb); + } + + /* When the connection is not being disconnected, simply stop + * trying to get replies and wait for the next loop tick. */ + break; + } + + /* Even if the context is subscribed, pending regular callbacks will + * get a reply before pub/sub messages arrive. */ + if (__redisShiftCallback(&ac->replies,&cb) != REDIS_OK) { + /* + * A spontaneous reply in a not-subscribed context can be the error + * reply that is sent when a new connection exceeds the maximum + * number of allowed connections on the server side. + * + * This is seen as an error instead of a regular reply because the + * server closes the connection after sending it. + * + * To prevent the error from being overwritten by an EOF error the + * connection is closed here. See issue #43. + * + * Another possibility is that the server is loading its dataset. + * In this case we also want to close the connection, and have the + * user wait until the server is ready to take our request. + */ + if (((redisReply*)reply)->type == REDIS_REPLY_ERROR) { + c->err = REDIS_ERR_OTHER; + snprintf(c->errstr,sizeof(c->errstr),"%s",((redisReply*)reply)->str); + __redisAsyncDisconnect(ac); + return; + } + /* No more regular callbacks and no errors, the context *must* be subscribed or monitoring. */ + assert((c->flags & REDIS_SUBSCRIBED || c->flags & REDIS_MONITORING)); + if(c->flags & REDIS_SUBSCRIBED) + __redisGetSubscribeCallback(ac,reply,&cb); + } + + if (cb.fn != NULL) { + __redisRunCallback(ac,&cb,reply); + c->reader->fn->freeObject(reply); + + /* Proceed with free'ing when redisAsyncFree() was called. */ + if (c->flags & REDIS_FREEING) { + __redisAsyncFree(ac); + return; + } + } else { + /* No callback for this reply. This can either be a NULL callback, + * or there were no callbacks to begin with. Either way, don't + * abort with an error, but simply ignore it because the client + * doesn't know what the server will spit out over the wire. */ + c->reader->fn->freeObject(reply); + } + } + + /* Disconnect when there was an error reading the reply */ + if (status != REDIS_OK) + __redisAsyncDisconnect(ac); +} + +/* Internal helper function to detect socket status the first time a read or + * write event fires. When connecting was not succesful, the connect callback + * is called with a REDIS_ERR status and the context is free'd. */ +static int __redisAsyncHandleConnect(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + + if (redisCheckSocketError(c,c->fd) == REDIS_ERR) { + /* Try again later when connect(2) is still in progress. */ + if (errno == EINPROGRESS) + return REDIS_OK; + + if (ac->onConnect) ac->onConnect(ac,REDIS_ERR); + __redisAsyncDisconnect(ac); + return REDIS_ERR; + } + + /* Mark context as connected. */ + c->flags |= REDIS_CONNECTED; + if (ac->onConnect) ac->onConnect(ac,REDIS_OK); + return REDIS_OK; +} + +/* This function should be called when the socket is readable. + * It processes all replies that can be read and executes their callbacks. + */ +void redisAsyncHandleRead(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + + if (!(c->flags & REDIS_CONNECTED)) { + /* Abort connect was not successful. */ + if (__redisAsyncHandleConnect(ac) != REDIS_OK) + return; + /* Try again later when the context is still not connected. */ + if (!(c->flags & REDIS_CONNECTED)) + return; + } + + if (redisBufferRead(c) == REDIS_ERR) { + __redisAsyncDisconnect(ac); + } else { + /* Always re-schedule reads */ + _EL_ADD_READ(ac); + redisProcessCallbacks(ac); + } +} + +void redisAsyncHandleWrite(redisAsyncContext *ac) { + redisContext *c = &(ac->c); + int done = 0; + + if (!(c->flags & REDIS_CONNECTED)) { + /* Abort connect was not successful. */ + if (__redisAsyncHandleConnect(ac) != REDIS_OK) + return; + /* Try again later when the context is still not connected. */ + if (!(c->flags & REDIS_CONNECTED)) + return; + } + + if (redisBufferWrite(c,&done) == REDIS_ERR) { + __redisAsyncDisconnect(ac); + } else { + /* Continue writing when not done, stop writing otherwise */ + if (!done) + _EL_ADD_WRITE(ac); + else + _EL_DEL_WRITE(ac); + + /* Always schedule reads after writes */ + _EL_ADD_READ(ac); + } +} + +/* Sets a pointer to the first argument and its length starting at p. Returns + * the number of bytes to skip to get to the following argument. */ +static char *nextArgument(char *start, char **str, size_t *len) { + char *p = start; + if (p[0] != '$') { + p = strchr(p,'$'); + if (p == NULL) return NULL; + } + + *len = (int)strtol(p+1,NULL,10); + p = strchr(p,'\r'); + assert(p); + *str = p+2; + return p+2+(*len)+2; +} + +/* Helper function for the redisAsyncCommand* family of functions. Writes a + * formatted command to the output buffer and registers the provided callback + * function with the context. */ +static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, char *cmd, size_t len) { + redisContext *c = &(ac->c); + redisCallback cb; + int pvariant, hasnext; + char *cstr, *astr; + size_t clen, alen; + char *p; + sds sname; + + /* Don't accept new commands when the connection is about to be closed. */ + if (c->flags & (REDIS_DISCONNECTING | REDIS_FREEING)) return REDIS_ERR; + + /* Setup callback */ + cb.fn = fn; + cb.privdata = privdata; + + /* Find out which command will be appended. */ + p = nextArgument(cmd,&cstr,&clen); + assert(p != NULL); + hasnext = (p[0] == '$'); + pvariant = (tolower(cstr[0]) == 'p') ? 1 : 0; + cstr += pvariant; + clen -= pvariant; + + if (hasnext && strncasecmp(cstr,"subscribe\r\n",11) == 0) { + c->flags |= REDIS_SUBSCRIBED; + + /* Add every channel/pattern to the list of subscription callbacks. */ + while ((p = nextArgument(p,&astr,&alen)) != NULL) { + sname = sdsnewlen(astr,alen); + if (pvariant) + dictReplace(ac->sub.patterns,sname,&cb); + else + dictReplace(ac->sub.channels,sname,&cb); + } + } else if (strncasecmp(cstr,"unsubscribe\r\n",13) == 0) { + /* It is only useful to call (P)UNSUBSCRIBE when the context is + * subscribed to one or more channels or patterns. */ + if (!(c->flags & REDIS_SUBSCRIBED)) return REDIS_ERR; + + /* (P)UNSUBSCRIBE does not have its own response: every channel or + * pattern that is unsubscribed will receive a message. This means we + * should not append a callback function for this command. */ + } else if(strncasecmp(cstr,"monitor\r\n",9) == 0) { + /* Set monitor flag and push callback */ + c->flags |= REDIS_MONITORING; + __redisPushCallback(&ac->replies,&cb); + } else { + if (c->flags & REDIS_SUBSCRIBED) + /* This will likely result in an error reply, but it needs to be + * received and passed to the callback. */ + __redisPushCallback(&ac->sub.invalid,&cb); + else + __redisPushCallback(&ac->replies,&cb); + } + + __redisAppendCommand(c,cmd,len); + + /* Always schedule a write when the write buffer is non-empty */ + _EL_ADD_WRITE(ac); + + return REDIS_OK; +} + +int redisvAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, va_list ap) { + char *cmd; + int len; + int status; + len = redisvFormatCommand(&cmd,format,ap); + status = __redisAsyncCommand(ac,fn,privdata,cmd,len); + free(cmd); + return status; +} + +int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...) { + va_list ap; + int status; + va_start(ap,format); + status = redisvAsyncCommand(ac,fn,privdata,format,ap); + va_end(ap); + return status; +} + +int redisAsyncCommandArgv(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen) { + char *cmd; + int len; + int status; + len = redisFormatCommandArgv(&cmd,argc,argv,argvlen); + status = __redisAsyncCommand(ac,fn,privdata,cmd,len); + free(cmd); + return status; +} diff --git a/data-server/external/hiredis/async.h b/data-server/external/hiredis/async.h new file mode 100755 index 0000000000..268274e8e7 --- /dev/null +++ b/data-server/external/hiredis/async.h @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2009-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; 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. + */ + +#ifndef __HIREDIS_ASYNC_H +#define __HIREDIS_ASYNC_H +#include "hiredis.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct redisAsyncContext; /* need forward declaration of redisAsyncContext */ +struct dict; /* dictionary header is included in async.c */ + +/* Reply callback prototype and container */ +typedef void (redisCallbackFn)(struct redisAsyncContext*, void*, void*); +typedef struct redisCallback { + struct redisCallback *next; /* simple singly linked list */ + redisCallbackFn *fn; + void *privdata; +} redisCallback; + +/* List of callbacks for either regular replies or pub/sub */ +typedef struct redisCallbackList { + redisCallback *head, *tail; +} redisCallbackList; + +/* Connection callback prototypes */ +typedef void (redisDisconnectCallback)(const struct redisAsyncContext*, int status); +typedef void (redisConnectCallback)(const struct redisAsyncContext*, int status); + +/* Context for an async connection to Redis */ +typedef struct redisAsyncContext { + /* Hold the regular context, so it can be realloc'ed. */ + redisContext c; + + /* Setup error flags so they can be used directly. */ + int err; + char *errstr; + + /* Not used by hiredis */ + void *data; + + /* Event library data and hooks */ + struct { + void *data; + + /* Hooks that are called when the library expects to start + * reading/writing. These functions should be idempotent. */ + void (*addRead)(void *privdata); + void (*delRead)(void *privdata); + void (*addWrite)(void *privdata); + void (*delWrite)(void *privdata); + void (*cleanup)(void *privdata); + } ev; + + /* Called when either the connection is terminated due to an error or per + * user request. The status is set accordingly (REDIS_OK, REDIS_ERR). */ + redisDisconnectCallback *onDisconnect; + + /* Called when the first write event was received. */ + redisConnectCallback *onConnect; + + /* Regular command callbacks */ + redisCallbackList replies; + + /* Subscription callbacks */ + struct { + redisCallbackList invalid; + struct dict *channels; + struct dict *patterns; + } sub; +} redisAsyncContext; + +/* Functions that proxy to hiredis */ +redisAsyncContext *redisAsyncConnect(const char *ip, int port); +redisAsyncContext *redisAsyncConnectUnix(const char *path); +int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn); +int redisAsyncSetDisconnectCallback(redisAsyncContext *ac, redisDisconnectCallback *fn); +void redisAsyncDisconnect(redisAsyncContext *ac); +void redisAsyncFree(redisAsyncContext *ac); + +/* Handle read/write events */ +void redisAsyncHandleRead(redisAsyncContext *ac); +void redisAsyncHandleWrite(redisAsyncContext *ac); + +/* Command functions for an async context. Write the command to the + * output buffer and register the provided callback. */ +int redisvAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, va_list ap); +int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...); +int redisAsyncCommandArgv(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, int argc, const char **argv, const size_t *argvlen); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/data-server/external/hiredis/dict.c b/data-server/external/hiredis/dict.c new file mode 100755 index 0000000000..79b1041cad --- /dev/null +++ b/data-server/external/hiredis/dict.c @@ -0,0 +1,338 @@ +/* Hash table implementation. + * + * This file implements in memory hash tables with insert/del/replace/find/ + * get-random-element operations. Hash tables will auto resize if needed + * tables of power of two in size are used, collisions are handled by + * chaining. See the source code for more information... :) + * + * Copyright (c) 2006-2010, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; 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. + */ + +#include "fmacros.h" +#include +#include +#include +#include "dict.h" + +/* -------------------------- private prototypes ---------------------------- */ + +static int _dictExpandIfNeeded(dict *ht); +static unsigned long _dictNextPower(unsigned long size); +static int _dictKeyIndex(dict *ht, const void *key); +static int _dictInit(dict *ht, dictType *type, void *privDataPtr); + +/* -------------------------- hash functions -------------------------------- */ + +/* Generic hash function (a popular one from Bernstein). + * I tested a few and this was the best. */ +static unsigned int dictGenHashFunction(const unsigned char *buf, int len) { + unsigned int hash = 5381; + + while (len--) + hash = ((hash << 5) + hash) + (*buf++); /* hash * 33 + c */ + return hash; +} + +/* ----------------------------- API implementation ------------------------- */ + +/* Reset an hashtable already initialized with ht_init(). + * NOTE: This function should only called by ht_destroy(). */ +static void _dictReset(dict *ht) { + ht->table = NULL; + ht->size = 0; + ht->sizemask = 0; + ht->used = 0; +} + +/* Create a new hash table */ +static dict *dictCreate(dictType *type, void *privDataPtr) { + dict *ht = malloc(sizeof(*ht)); + _dictInit(ht,type,privDataPtr); + return ht; +} + +/* Initialize the hash table */ +static int _dictInit(dict *ht, dictType *type, void *privDataPtr) { + _dictReset(ht); + ht->type = type; + ht->privdata = privDataPtr; + return DICT_OK; +} + +/* Expand or create the hashtable */ +static int dictExpand(dict *ht, unsigned long size) { + dict n; /* the new hashtable */ + unsigned long realsize = _dictNextPower(size), i; + + /* the size is invalid if it is smaller than the number of + * elements already inside the hashtable */ + if (ht->used > size) + return DICT_ERR; + + _dictInit(&n, ht->type, ht->privdata); + n.size = realsize; + n.sizemask = realsize-1; + n.table = calloc(realsize,sizeof(dictEntry*)); + + /* Copy all the elements from the old to the new table: + * note that if the old hash table is empty ht->size is zero, + * so dictExpand just creates an hash table. */ + n.used = ht->used; + for (i = 0; i < ht->size && ht->used > 0; i++) { + dictEntry *he, *nextHe; + + if (ht->table[i] == NULL) continue; + + /* For each hash entry on this slot... */ + he = ht->table[i]; + while(he) { + unsigned int h; + + nextHe = he->next; + /* Get the new element index */ + h = dictHashKey(ht, he->key) & n.sizemask; + he->next = n.table[h]; + n.table[h] = he; + ht->used--; + /* Pass to the next element */ + he = nextHe; + } + } + assert(ht->used == 0); + free(ht->table); + + /* Remap the new hashtable in the old */ + *ht = n; + return DICT_OK; +} + +/* Add an element to the target hash table */ +static int dictAdd(dict *ht, void *key, void *val) { + int index; + dictEntry *entry; + + /* Get the index of the new element, or -1 if + * the element already exists. */ + if ((index = _dictKeyIndex(ht, key)) == -1) + return DICT_ERR; + + /* Allocates the memory and stores key */ + entry = malloc(sizeof(*entry)); + entry->next = ht->table[index]; + ht->table[index] = entry; + + /* Set the hash entry fields. */ + dictSetHashKey(ht, entry, key); + dictSetHashVal(ht, entry, val); + ht->used++; + return DICT_OK; +} + +/* Add an element, discarding the old if the key already exists. + * Return 1 if the key was added from scratch, 0 if there was already an + * element with such key and dictReplace() just performed a value update + * operation. */ +static int dictReplace(dict *ht, void *key, void *val) { + dictEntry *entry, auxentry; + + /* Try to add the element. If the key + * does not exists dictAdd will suceed. */ + if (dictAdd(ht, key, val) == DICT_OK) + return 1; + /* It already exists, get the entry */ + entry = dictFind(ht, key); + /* Free the old value and set the new one */ + /* Set the new value and free the old one. Note that it is important + * to do that in this order, as the value may just be exactly the same + * as the previous one. In this context, think to reference counting, + * you want to increment (set), and then decrement (free), and not the + * reverse. */ + auxentry = *entry; + dictSetHashVal(ht, entry, val); + dictFreeEntryVal(ht, &auxentry); + return 0; +} + +/* Search and remove an element */ +static int dictDelete(dict *ht, const void *key) { + unsigned int h; + dictEntry *de, *prevde; + + if (ht->size == 0) + return DICT_ERR; + h = dictHashKey(ht, key) & ht->sizemask; + de = ht->table[h]; + + prevde = NULL; + while(de) { + if (dictCompareHashKeys(ht,key,de->key)) { + /* Unlink the element from the list */ + if (prevde) + prevde->next = de->next; + else + ht->table[h] = de->next; + + dictFreeEntryKey(ht,de); + dictFreeEntryVal(ht,de); + free(de); + ht->used--; + return DICT_OK; + } + prevde = de; + de = de->next; + } + return DICT_ERR; /* not found */ +} + +/* Destroy an entire hash table */ +static int _dictClear(dict *ht) { + unsigned long i; + + /* Free all the elements */ + for (i = 0; i < ht->size && ht->used > 0; i++) { + dictEntry *he, *nextHe; + + if ((he = ht->table[i]) == NULL) continue; + while(he) { + nextHe = he->next; + dictFreeEntryKey(ht, he); + dictFreeEntryVal(ht, he); + free(he); + ht->used--; + he = nextHe; + } + } + /* Free the table and the allocated cache structure */ + free(ht->table); + /* Re-initialize the table */ + _dictReset(ht); + return DICT_OK; /* never fails */ +} + +/* Clear & Release the hash table */ +static void dictRelease(dict *ht) { + _dictClear(ht); + free(ht); +} + +static dictEntry *dictFind(dict *ht, const void *key) { + dictEntry *he; + unsigned int h; + + if (ht->size == 0) return NULL; + h = dictHashKey(ht, key) & ht->sizemask; + he = ht->table[h]; + while(he) { + if (dictCompareHashKeys(ht, key, he->key)) + return he; + he = he->next; + } + return NULL; +} + +static dictIterator *dictGetIterator(dict *ht) { + dictIterator *iter = malloc(sizeof(*iter)); + + iter->ht = ht; + iter->index = -1; + iter->entry = NULL; + iter->nextEntry = NULL; + return iter; +} + +static dictEntry *dictNext(dictIterator *iter) { + while (1) { + if (iter->entry == NULL) { + iter->index++; + if (iter->index >= + (signed)iter->ht->size) break; + iter->entry = iter->ht->table[iter->index]; + } else { + iter->entry = iter->nextEntry; + } + if (iter->entry) { + /* We need to save the 'next' here, the iterator user + * may delete the entry we are returning. */ + iter->nextEntry = iter->entry->next; + return iter->entry; + } + } + return NULL; +} + +static void dictReleaseIterator(dictIterator *iter) { + free(iter); +} + +/* ------------------------- private functions ------------------------------ */ + +/* Expand the hash table if needed */ +static int _dictExpandIfNeeded(dict *ht) { + /* If the hash table is empty expand it to the intial size, + * if the table is "full" dobule its size. */ + if (ht->size == 0) + return dictExpand(ht, DICT_HT_INITIAL_SIZE); + if (ht->used == ht->size) + return dictExpand(ht, ht->size*2); + return DICT_OK; +} + +/* Our hash table capability is a power of two */ +static unsigned long _dictNextPower(unsigned long size) { + unsigned long i = DICT_HT_INITIAL_SIZE; + + if (size >= LONG_MAX) return LONG_MAX; + while(1) { + if (i >= size) + return i; + i *= 2; + } +} + +/* Returns the index of a free slot that can be populated with + * an hash entry for the given 'key'. + * If the key already exists, -1 is returned. */ +static int _dictKeyIndex(dict *ht, const void *key) { + unsigned int h; + dictEntry *he; + + /* Expand the hashtable if needed */ + if (_dictExpandIfNeeded(ht) == DICT_ERR) + return -1; + /* Compute the key hash value */ + h = dictHashKey(ht, key) & ht->sizemask; + /* Search if this slot does not already contain the given key */ + he = ht->table[h]; + while(he) { + if (dictCompareHashKeys(ht, key, he->key)) + return -1; + he = he->next; + } + return h; +} + diff --git a/data-server/external/hiredis/dict.h b/data-server/external/hiredis/dict.h new file mode 100755 index 0000000000..95fcd280e2 --- /dev/null +++ b/data-server/external/hiredis/dict.h @@ -0,0 +1,126 @@ +/* Hash table implementation. + * + * This file implements in memory hash tables with insert/del/replace/find/ + * get-random-element operations. Hash tables will auto resize if needed + * tables of power of two in size are used, collisions are handled by + * chaining. See the source code for more information... :) + * + * Copyright (c) 2006-2010, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; 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. + */ + +#ifndef __DICT_H +#define __DICT_H + +#define DICT_OK 0 +#define DICT_ERR 1 + +/* Unused arguments generate annoying warnings... */ +#define DICT_NOTUSED(V) ((void) V) + +typedef struct dictEntry { + void *key; + void *val; + struct dictEntry *next; +} dictEntry; + +typedef struct dictType { + unsigned int (*hashFunction)(const void *key); + void *(*keyDup)(void *privdata, const void *key); + void *(*valDup)(void *privdata, const void *obj); + int (*keyCompare)(void *privdata, const void *key1, const void *key2); + void (*keyDestructor)(void *privdata, void *key); + void (*valDestructor)(void *privdata, void *obj); +} dictType; + +typedef struct dict { + dictEntry **table; + dictType *type; + unsigned long size; + unsigned long sizemask; + unsigned long used; + void *privdata; +} dict; + +typedef struct dictIterator { + dict *ht; + int index; + dictEntry *entry, *nextEntry; +} dictIterator; + +/* This is the initial size of every hash table */ +#define DICT_HT_INITIAL_SIZE 4 + +/* ------------------------------- Macros ------------------------------------*/ +#define dictFreeEntryVal(ht, entry) \ + if ((ht)->type->valDestructor) \ + (ht)->type->valDestructor((ht)->privdata, (entry)->val) + +#define dictSetHashVal(ht, entry, _val_) do { \ + if ((ht)->type->valDup) \ + entry->val = (ht)->type->valDup((ht)->privdata, _val_); \ + else \ + entry->val = (_val_); \ +} while(0) + +#define dictFreeEntryKey(ht, entry) \ + if ((ht)->type->keyDestructor) \ + (ht)->type->keyDestructor((ht)->privdata, (entry)->key) + +#define dictSetHashKey(ht, entry, _key_) do { \ + if ((ht)->type->keyDup) \ + entry->key = (ht)->type->keyDup((ht)->privdata, _key_); \ + else \ + entry->key = (_key_); \ +} while(0) + +#define dictCompareHashKeys(ht, key1, key2) \ + (((ht)->type->keyCompare) ? \ + (ht)->type->keyCompare((ht)->privdata, key1, key2) : \ + (key1) == (key2)) + +#define dictHashKey(ht, key) (ht)->type->hashFunction(key) + +#define dictGetEntryKey(he) ((he)->key) +#define dictGetEntryVal(he) ((he)->val) +#define dictSlots(ht) ((ht)->size) +#define dictSize(ht) ((ht)->used) + +/* API */ +static unsigned int dictGenHashFunction(const unsigned char *buf, int len); +static dict *dictCreate(dictType *type, void *privDataPtr); +static int dictExpand(dict *ht, unsigned long size); +static int dictAdd(dict *ht, void *key, void *val); +static int dictReplace(dict *ht, void *key, void *val); +static int dictDelete(dict *ht, const void *key); +static void dictRelease(dict *ht); +static dictEntry * dictFind(dict *ht, const void *key); +static dictIterator *dictGetIterator(dict *ht); +static dictEntry *dictNext(dictIterator *iter); +static void dictReleaseIterator(dictIterator *iter); + +#endif /* __DICT_H */ diff --git a/data-server/external/hiredis/fmacros.h b/data-server/external/hiredis/fmacros.h new file mode 100755 index 0000000000..799c12c1c0 --- /dev/null +++ b/data-server/external/hiredis/fmacros.h @@ -0,0 +1,20 @@ +#ifndef __HIREDIS_FMACRO_H +#define __HIREDIS_FMACRO_H + +#if !defined(_BSD_SOURCE) +#define _BSD_SOURCE +#endif + +#if defined(__sun__) +#define _POSIX_C_SOURCE 200112L +#elif defined(__linux__) +#define _XOPEN_SOURCE 600 +#else +#define _XOPEN_SOURCE +#endif + +#if __APPLE__ && __MACH__ +#define _OSX +#endif + +#endif diff --git a/data-server/external/hiredis/hiredis.c b/data-server/external/hiredis/hiredis.c new file mode 100755 index 0000000000..9b74b5b775 --- /dev/null +++ b/data-server/external/hiredis/hiredis.c @@ -0,0 +1,1322 @@ +/* + * Copyright (c) 2009-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; 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. + */ + +#include "fmacros.h" +#include +#include +#include +#include +#include +#include + +#include "hiredis.h" +#include "net.h" +#include "sds.h" + +static redisReply *createReplyObject(int type); +static void *createStringObject(const redisReadTask *task, char *str, size_t len); +static void *createArrayObject(const redisReadTask *task, int elements); +static void *createIntegerObject(const redisReadTask *task, long long value); +static void *createNilObject(const redisReadTask *task); + +/* Default set of functions to build the reply. Keep in mind that such a + * function returning NULL is interpreted as OOM. */ +static redisReplyObjectFunctions defaultFunctions = { + createStringObject, + createArrayObject, + createIntegerObject, + createNilObject, + freeReplyObject +}; + +/* Create a reply object */ +static redisReply *createReplyObject(int type) { + redisReply *r = calloc(1,sizeof(*r)); + + if (r == NULL) + return NULL; + + r->type = type; + return r; +} + +/* Free a reply object */ +void freeReplyObject(void *reply) { + redisReply *r = reply; + size_t j; + + switch(r->type) { + case REDIS_REPLY_INTEGER: + break; /* Nothing to free */ + case REDIS_REPLY_ARRAY: + if (r->element != NULL) { + for (j = 0; j < r->elements; j++) + if (r->element[j] != NULL) + freeReplyObject(r->element[j]); + free(r->element); + } + break; + case REDIS_REPLY_ERROR: + case REDIS_REPLY_STATUS: + case REDIS_REPLY_STRING: + if (r->str != NULL) + free(r->str); + break; + } + free(r); +} + +static void *createStringObject(const redisReadTask *task, char *str, size_t len) { + redisReply *r, *parent; + char *buf; + + r = createReplyObject(task->type); + if (r == NULL) + return NULL; + + buf = malloc(len+1); + if (buf == NULL) { + freeReplyObject(r); + return NULL; + } + + assert(task->type == REDIS_REPLY_ERROR || + task->type == REDIS_REPLY_STATUS || + task->type == REDIS_REPLY_STRING); + + /* Copy string value */ + memcpy(buf,str,len); + buf[len] = '\0'; + r->str = buf; + r->len = len; + + if (task->parent) { + parent = task->parent->obj; + assert(parent->type == REDIS_REPLY_ARRAY); + parent->element[task->idx] = r; + } + return r; +} + +static void *createArrayObject(const redisReadTask *task, int elements) { + redisReply *r, *parent; + + r = createReplyObject(REDIS_REPLY_ARRAY); + if (r == NULL) + return NULL; + + if (elements > 0) { + r->element = calloc(elements,sizeof(redisReply*)); + if (r->element == NULL) { + freeReplyObject(r); + return NULL; + } + } + + r->elements = elements; + + if (task->parent) { + parent = task->parent->obj; + assert(parent->type == REDIS_REPLY_ARRAY); + parent->element[task->idx] = r; + } + return r; +} + +static void *createIntegerObject(const redisReadTask *task, long long value) { + redisReply *r, *parent; + + r = createReplyObject(REDIS_REPLY_INTEGER); + if (r == NULL) + return NULL; + + r->integer = value; + + if (task->parent) { + parent = task->parent->obj; + assert(parent->type == REDIS_REPLY_ARRAY); + parent->element[task->idx] = r; + } + return r; +} + +static void *createNilObject(const redisReadTask *task) { + redisReply *r, *parent; + + r = createReplyObject(REDIS_REPLY_NIL); + if (r == NULL) + return NULL; + + if (task->parent) { + parent = task->parent->obj; + assert(parent->type == REDIS_REPLY_ARRAY); + parent->element[task->idx] = r; + } + return r; +} + +static void __redisReaderSetError(redisReader *r, int type, const char *str) { + size_t len; + + if (r->reply != NULL && r->fn && r->fn->freeObject) { + r->fn->freeObject(r->reply); + r->reply = NULL; + } + + /* Clear input buffer on errors. */ + if (r->buf != NULL) { + sdsfree(r->buf); + r->buf = NULL; + r->pos = r->len = 0; + } + + /* Reset task stack. */ + r->ridx = -1; + + /* Set error. */ + r->err = type; + len = strlen(str); + len = len < (sizeof(r->errstr)-1) ? len : (sizeof(r->errstr)-1); + memcpy(r->errstr,str,len); + r->errstr[len] = '\0'; +} + +static size_t chrtos(char *buf, size_t size, char byte) { + size_t len = 0; + + switch(byte) { + case '\\': + case '"': + len = snprintf(buf,size,"\"\\%c\"",byte); + break; + case '\n': len = snprintf(buf,size,"\"\\n\""); break; + case '\r': len = snprintf(buf,size,"\"\\r\""); break; + case '\t': len = snprintf(buf,size,"\"\\t\""); break; + case '\a': len = snprintf(buf,size,"\"\\a\""); break; + case '\b': len = snprintf(buf,size,"\"\\b\""); break; + default: + if (isprint(byte)) + len = snprintf(buf,size,"\"%c\"",byte); + else + len = snprintf(buf,size,"\"\\x%02x\"",(unsigned char)byte); + break; + } + + return len; +} + +static void __redisReaderSetErrorProtocolByte(redisReader *r, char byte) { + char cbuf[8], sbuf[128]; + + chrtos(cbuf,sizeof(cbuf),byte); + snprintf(sbuf,sizeof(sbuf), + "Protocol error, got %s as reply type byte", cbuf); + __redisReaderSetError(r,REDIS_ERR_PROTOCOL,sbuf); +} + +static void __redisReaderSetErrorOOM(redisReader *r) { + __redisReaderSetError(r,REDIS_ERR_OOM,"Out of memory"); +} + +static char *readBytes(redisReader *r, unsigned int bytes) { + char *p; + if (r->len-r->pos >= bytes) { + p = r->buf+r->pos; + r->pos += bytes; + return p; + } + return NULL; +} + +/* Find pointer to \r\n. */ +static char *seekNewline(char *s, size_t len) { + int pos = 0; + int _len = len-1; + + /* Position should be < len-1 because the character at "pos" should be + * followed by a \n. Note that strchr cannot be used because it doesn't + * allow to search a limited length and the buffer that is being searched + * might not have a trailing NULL character. */ + while (pos < _len) { + while(pos < _len && s[pos] != '\r') pos++; + if (s[pos] != '\r') { + /* Not found. */ + return NULL; + } else { + if (s[pos+1] == '\n') { + /* Found. */ + return s+pos; + } else { + /* Continue searching. */ + pos++; + } + } + } + return NULL; +} + +/* Read a long long value starting at *s, under the assumption that it will be + * terminated by \r\n. Ambiguously returns -1 for unexpected input. */ +static long long readLongLong(char *s) { + long long v = 0; + int dec, mult = 1; + char c; + + if (*s == '-') { + mult = -1; + s++; + } else if (*s == '+') { + mult = 1; + s++; + } + + while ((c = *(s++)) != '\r') { + dec = c - '0'; + if (dec >= 0 && dec < 10) { + v *= 10; + v += dec; + } else { + /* Should not happen... */ + return -1; + } + } + + return mult*v; +} + +static char *readLine(redisReader *r, int *_len) { + char *p, *s; + int len; + + p = r->buf+r->pos; + s = seekNewline(p,(r->len-r->pos)); + if (s != NULL) { + len = s-(r->buf+r->pos); + r->pos += len+2; /* skip \r\n */ + if (_len) *_len = len; + return p; + } + return NULL; +} + +static void moveToNextTask(redisReader *r) { + redisReadTask *cur, *prv; + while (r->ridx >= 0) { + /* Return a.s.a.p. when the stack is now empty. */ + if (r->ridx == 0) { + r->ridx--; + return; + } + + cur = &(r->rstack[r->ridx]); + prv = &(r->rstack[r->ridx-1]); + assert(prv->type == REDIS_REPLY_ARRAY); + if (cur->idx == prv->elements-1) { + r->ridx--; + } else { + /* Reset the type because the next item can be anything */ + assert(cur->idx < prv->elements); + cur->type = -1; + cur->elements = -1; + cur->idx++; + return; + } + } +} + +static int processLineItem(redisReader *r) { + redisReadTask *cur = &(r->rstack[r->ridx]); + void *obj; + char *p; + int len; + + if ((p = readLine(r,&len)) != NULL) { + if (cur->type == REDIS_REPLY_INTEGER) { + if (r->fn && r->fn->createInteger) + obj = r->fn->createInteger(cur,readLongLong(p)); + else + obj = (void*)REDIS_REPLY_INTEGER; + } else { + /* Type will be error or status. */ + if (r->fn && r->fn->createString) + obj = r->fn->createString(cur,p,len); + else + obj = (void*)(size_t)(cur->type); + } + + if (obj == NULL) { + __redisReaderSetErrorOOM(r); + return REDIS_ERR; + } + + /* Set reply if this is the root object. */ + if (r->ridx == 0) r->reply = obj; + moveToNextTask(r); + return REDIS_OK; + } + + return REDIS_ERR; +} + +static int processBulkItem(redisReader *r) { + redisReadTask *cur = &(r->rstack[r->ridx]); + void *obj = NULL; + char *p, *s; + long len; + unsigned long bytelen; + int success = 0; + + p = r->buf+r->pos; + s = seekNewline(p,r->len-r->pos); + if (s != NULL) { + p = r->buf+r->pos; + bytelen = s-(r->buf+r->pos)+2; /* include \r\n */ + len = readLongLong(p); + + if (len < 0) { + /* The nil object can always be created. */ + if (r->fn && r->fn->createNil) + obj = r->fn->createNil(cur); + else + obj = (void*)REDIS_REPLY_NIL; + success = 1; + } else { + /* Only continue when the buffer contains the entire bulk item. */ + bytelen += len+2; /* include \r\n */ + if (r->pos+bytelen <= r->len) { + if (r->fn && r->fn->createString) + obj = r->fn->createString(cur,s+2,len); + else + obj = (void*)REDIS_REPLY_STRING; + success = 1; + } + } + + /* Proceed when obj was created. */ + if (success) { + if (obj == NULL) { + __redisReaderSetErrorOOM(r); + return REDIS_ERR; + } + + r->pos += bytelen; + + /* Set reply if this is the root object. */ + if (r->ridx == 0) r->reply = obj; + moveToNextTask(r); + return REDIS_OK; + } + } + + return REDIS_ERR; +} + +static int processMultiBulkItem(redisReader *r) { + redisReadTask *cur = &(r->rstack[r->ridx]); + void *obj; + char *p; + long elements; + int root = 0; + + /* Set error for nested multi bulks with depth > 7 */ + if (r->ridx == 8) { + __redisReaderSetError(r,REDIS_ERR_PROTOCOL, + "No support for nested multi bulk replies with depth > 7"); + return REDIS_ERR; + } + + if ((p = readLine(r,NULL)) != NULL) { + elements = readLongLong(p); + root = (r->ridx == 0); + + if (elements == -1) { + if (r->fn && r->fn->createNil) + obj = r->fn->createNil(cur); + else + obj = (void*)REDIS_REPLY_NIL; + + if (obj == NULL) { + __redisReaderSetErrorOOM(r); + return REDIS_ERR; + } + + moveToNextTask(r); + } else { + if (r->fn && r->fn->createArray) + obj = r->fn->createArray(cur,elements); + else + obj = (void*)REDIS_REPLY_ARRAY; + + if (obj == NULL) { + __redisReaderSetErrorOOM(r); + return REDIS_ERR; + } + + /* Modify task stack when there are more than 0 elements. */ + if (elements > 0) { + cur->elements = elements; + cur->obj = obj; + r->ridx++; + r->rstack[r->ridx].type = -1; + r->rstack[r->ridx].elements = -1; + r->rstack[r->ridx].idx = 0; + r->rstack[r->ridx].obj = NULL; + r->rstack[r->ridx].parent = cur; + r->rstack[r->ridx].privdata = r->privdata; + } else { + moveToNextTask(r); + } + } + + /* Set reply if this is the root object. */ + if (root) r->reply = obj; + return REDIS_OK; + } + + return REDIS_ERR; +} + +static int processItem(redisReader *r) { + redisReadTask *cur = &(r->rstack[r->ridx]); + char *p; + + /* check if we need to read type */ + if (cur->type < 0) { + if ((p = readBytes(r,1)) != NULL) { + switch (p[0]) { + case '-': + cur->type = REDIS_REPLY_ERROR; + break; + case '+': + cur->type = REDIS_REPLY_STATUS; + break; + case ':': + cur->type = REDIS_REPLY_INTEGER; + break; + case '$': + cur->type = REDIS_REPLY_STRING; + break; + case '*': + cur->type = REDIS_REPLY_ARRAY; + break; + default: + __redisReaderSetErrorProtocolByte(r,*p); + return REDIS_ERR; + } + } else { + /* could not consume 1 byte */ + return REDIS_ERR; + } + } + + /* process typed item */ + switch(cur->type) { + case REDIS_REPLY_ERROR: + case REDIS_REPLY_STATUS: + case REDIS_REPLY_INTEGER: + return processLineItem(r); + case REDIS_REPLY_STRING: + return processBulkItem(r); + case REDIS_REPLY_ARRAY: + return processMultiBulkItem(r); + default: + assert(NULL); + return REDIS_ERR; /* Avoid warning. */ + } +} + +redisReader *redisReaderCreate(void) { + redisReader *r; + + r = calloc(sizeof(redisReader),1); + if (r == NULL) + return NULL; + + r->err = 0; + r->errstr[0] = '\0'; + r->fn = &defaultFunctions; + r->buf = sdsempty(); + r->maxbuf = REDIS_READER_MAX_BUF; + if (r->buf == NULL) { + free(r); + return NULL; + } + + r->ridx = -1; + return r; +} + +void redisReaderFree(redisReader *r) { + if (r->reply != NULL && r->fn && r->fn->freeObject) + r->fn->freeObject(r->reply); + if (r->buf != NULL) + sdsfree(r->buf); + free(r); +} + +int redisReaderFeed(redisReader *r, const char *buf, size_t len) { + sds newbuf; + + /* Return early when this reader is in an erroneous state. */ + if (r->err) + return REDIS_ERR; + + /* Copy the provided buffer. */ + if (buf != NULL && len >= 1) { + /* Destroy internal buffer when it is empty and is quite large. */ + if (r->len == 0 && r->maxbuf != 0 && sdsavail(r->buf) > r->maxbuf) { + sdsfree(r->buf); + r->buf = sdsempty(); + r->pos = 0; + + /* r->buf should not be NULL since we just free'd a larger one. */ + assert(r->buf != NULL); + } + + newbuf = sdscatlen(r->buf,buf,len); + if (newbuf == NULL) { + __redisReaderSetErrorOOM(r); + return REDIS_ERR; + } + + r->buf = newbuf; + r->len = sdslen(r->buf); + } + + return REDIS_OK; +} + +int redisReaderGetReply(redisReader *r, void **reply) { + /* Default target pointer to NULL. */ + if (reply != NULL) + *reply = NULL; + + /* Return early when this reader is in an erroneous state. */ + if (r->err) + return REDIS_ERR; + + /* When the buffer is empty, there will never be a reply. */ + if (r->len == 0) + return REDIS_OK; + + /* Set first item to process when the stack is empty. */ + if (r->ridx == -1) { + r->rstack[0].type = -1; + r->rstack[0].elements = -1; + r->rstack[0].idx = -1; + r->rstack[0].obj = NULL; + r->rstack[0].parent = NULL; + r->rstack[0].privdata = r->privdata; + r->ridx = 0; + } + + /* Process items in reply. */ + while (r->ridx >= 0) + if (processItem(r) != REDIS_OK) + break; + + /* Return ASAP when an error occurred. */ + if (r->err) + return REDIS_ERR; + + /* Discard part of the buffer when we've consumed at least 1k, to avoid + * doing unnecessary calls to memmove() in sds.c. */ + if (r->pos >= 1024) { + r->buf = sdsrange(r->buf,r->pos,-1); + r->pos = 0; + r->len = sdslen(r->buf); + } + + /* Emit a reply when there is one. */ + if (r->ridx == -1) { + if (reply != NULL) + *reply = r->reply; + r->reply = NULL; + } + return REDIS_OK; +} + +/* Calculate the number of bytes needed to represent an integer as string. */ +static int intlen(int i) { + int len = 0; + if (i < 0) { + len++; + i = -i; + } + do { + len++; + i /= 10; + } while(i); + return len; +} + +/* Helper that calculates the bulk length given a certain string length. */ +static size_t bulklen(size_t len) { + return 1+intlen(len)+2+len+2; +} + +int redisvFormatCommand(char **target, const char *format, va_list ap) { + const char *c = format; + char *cmd = NULL; /* final command */ + int pos; /* position in final command */ + sds curarg, newarg; /* current argument */ + int touched = 0; /* was the current argument touched? */ + char **curargv = NULL, **newargv = NULL; + int argc = 0; + int totlen = 0; + int j; + + /* Abort if there is not target to set */ + if (target == NULL) + return -1; + + /* Build the command string accordingly to protocol */ + curarg = sdsempty(); + if (curarg == NULL) + return -1; + + while(*c != '\0') { + if (*c != '%' || c[1] == '\0') { + if (*c == ' ') { + if (touched) { + newargv = realloc(curargv,sizeof(char*)*(argc+1)); + if (newargv == NULL) goto err; + curargv = newargv; + curargv[argc++] = curarg; + totlen += bulklen(sdslen(curarg)); + + /* curarg is put in argv so it can be overwritten. */ + curarg = sdsempty(); + if (curarg == NULL) goto err; + touched = 0; + } + } else { + newarg = sdscatlen(curarg,c,1); + if (newarg == NULL) goto err; + curarg = newarg; + touched = 1; + } + } else { + char *arg; + size_t size; + + /* Set newarg so it can be checked even if it is not touched. */ + newarg = curarg; + + switch(c[1]) { + case 's': + arg = va_arg(ap,char*); + size = strlen(arg); + if (size > 0) + newarg = sdscatlen(curarg,arg,size); + break; + case 'b': + arg = va_arg(ap,char*); + size = va_arg(ap,size_t); + if (size > 0) + newarg = sdscatlen(curarg,arg,size); + break; + case '%': + newarg = sdscat(curarg,"%"); + break; + default: + /* Try to detect printf format */ + { + static const char intfmts[] = "diouxX"; + char _format[16]; + const char *_p = c+1; + size_t _l = 0; + va_list _cpy; + + /* Flags */ + if (*_p != '\0' && *_p == '#') _p++; + if (*_p != '\0' && *_p == '0') _p++; + if (*_p != '\0' && *_p == '-') _p++; + if (*_p != '\0' && *_p == ' ') _p++; + if (*_p != '\0' && *_p == '+') _p++; + + /* Field width */ + while (*_p != '\0' && isdigit(*_p)) _p++; + + /* Precision */ + if (*_p == '.') { + _p++; + while (*_p != '\0' && isdigit(*_p)) _p++; + } + + /* Copy va_list before consuming with va_arg */ + va_copy(_cpy,ap); + + /* Integer conversion (without modifiers) */ + if (strchr(intfmts,*_p) != NULL) { + va_arg(ap,int); + goto fmt_valid; + } + + /* Double conversion (without modifiers) */ + if (strchr("eEfFgGaA",*_p) != NULL) { + va_arg(ap,double); + goto fmt_valid; + } + + /* Size: char */ + if (_p[0] == 'h' && _p[1] == 'h') { + _p += 2; + if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { + va_arg(ap,int); /* char gets promoted to int */ + goto fmt_valid; + } + goto fmt_invalid; + } + + /* Size: short */ + if (_p[0] == 'h') { + _p += 1; + if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { + va_arg(ap,int); /* short gets promoted to int */ + goto fmt_valid; + } + goto fmt_invalid; + } + + /* Size: long long */ + if (_p[0] == 'l' && _p[1] == 'l') { + _p += 2; + if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { + va_arg(ap,long long); + goto fmt_valid; + } + goto fmt_invalid; + } + + /* Size: long */ + if (_p[0] == 'l') { + _p += 1; + if (*_p != '\0' && strchr(intfmts,*_p) != NULL) { + va_arg(ap,long); + goto fmt_valid; + } + goto fmt_invalid; + } + + fmt_invalid: + va_end(_cpy); + goto err; + + fmt_valid: + _l = (_p+1)-c; + if (_l < sizeof(_format)-2) { + memcpy(_format,c,_l); + _format[_l] = '\0'; + newarg = sdscatvprintf(curarg,_format,_cpy); + + /* Update current position (note: outer blocks + * increment c twice so compensate here) */ + c = _p-1; + } + + va_end(_cpy); + break; + } + } + + if (newarg == NULL) goto err; + curarg = newarg; + + touched = 1; + c++; + } + c++; + } + + /* Add the last argument if needed */ + if (touched) { + newargv = realloc(curargv,sizeof(char*)*(argc+1)); + if (newargv == NULL) goto err; + curargv = newargv; + curargv[argc++] = curarg; + totlen += bulklen(sdslen(curarg)); + } else { + sdsfree(curarg); + } + + /* Clear curarg because it was put in curargv or was free'd. */ + curarg = NULL; + + /* Add bytes needed to hold multi bulk count */ + totlen += 1+intlen(argc)+2; + + /* Build the command at protocol level */ + cmd = malloc(totlen+1); + if (cmd == NULL) goto err; + + pos = sprintf(cmd,"*%d\r\n",argc); + for (j = 0; j < argc; j++) { + pos += sprintf(cmd+pos,"$%zu\r\n",sdslen(curargv[j])); + memcpy(cmd+pos,curargv[j],sdslen(curargv[j])); + pos += sdslen(curargv[j]); + sdsfree(curargv[j]); + cmd[pos++] = '\r'; + cmd[pos++] = '\n'; + } + assert(pos == totlen); + cmd[pos] = '\0'; + + free(curargv); + *target = cmd; + return totlen; + +err: + while(argc--) + sdsfree(curargv[argc]); + free(curargv); + + if (curarg != NULL) + sdsfree(curarg); + + /* No need to check cmd since it is the last statement that can fail, + * but do it anyway to be as defensive as possible. */ + if (cmd != NULL) + free(cmd); + + return -1; +} + +/* Format a command according to the Redis protocol. This function + * takes a format similar to printf: + * + * %s represents a C null terminated string you want to interpolate + * %b represents a binary safe string + * + * When using %b you need to provide both the pointer to the string + * and the length in bytes as a size_t. Examples: + * + * len = redisFormatCommand(target, "GET %s", mykey); + * len = redisFormatCommand(target, "SET %s %b", mykey, myval, myvallen); + */ +int redisFormatCommand(char **target, const char *format, ...) { + va_list ap; + int len; + va_start(ap,format); + len = redisvFormatCommand(target,format,ap); + va_end(ap); + return len; +} + +/* Format a command according to the Redis protocol. This function takes the + * number of arguments, an array with arguments and an array with their + * lengths. If the latter is set to NULL, strlen will be used to compute the + * argument lengths. + */ +int redisFormatCommandArgv(char **target, int argc, const char **argv, const size_t *argvlen) { + char *cmd = NULL; /* final command */ + int pos; /* position in final command */ + size_t len; + int totlen, j; + + /* Calculate number of bytes needed for the command */ + totlen = 1+intlen(argc)+2; + for (j = 0; j < argc; j++) { + len = argvlen ? argvlen[j] : strlen(argv[j]); + totlen += bulklen(len); + } + + /* Build the command at protocol level */ + cmd = malloc(totlen+1); + if (cmd == NULL) + return -1; + + pos = sprintf(cmd,"*%d\r\n",argc); + for (j = 0; j < argc; j++) { + len = argvlen ? argvlen[j] : strlen(argv[j]); + pos += sprintf(cmd+pos,"$%zu\r\n",len); + memcpy(cmd+pos,argv[j],len); + pos += len; + cmd[pos++] = '\r'; + cmd[pos++] = '\n'; + } + assert(pos == totlen); + cmd[pos] = '\0'; + + *target = cmd; + return totlen; +} + +void __redisSetError(redisContext *c, int type, const char *str) { + size_t len; + + c->err = type; + if (str != NULL) { + len = strlen(str); + len = len < (sizeof(c->errstr)-1) ? len : (sizeof(c->errstr)-1); + memcpy(c->errstr,str,len); + c->errstr[len] = '\0'; + } else { + /* Only REDIS_ERR_IO may lack a description! */ + assert(type == REDIS_ERR_IO); + strerror_r(errno,c->errstr,sizeof(c->errstr)); + } +} + +static redisContext *redisContextInit(void) { + redisContext *c; + + c = calloc(1,sizeof(redisContext)); + if (c == NULL) + return NULL; + + c->err = 0; + c->errstr[0] = '\0'; + c->obuf = sdsempty(); + c->reader = redisReaderCreate(); + return c; +} + +void redisFree(redisContext *c) { + if (c->fd > 0) + close(c->fd); + if (c->obuf != NULL) + sdsfree(c->obuf); + if (c->reader != NULL) + redisReaderFree(c->reader); + free(c); +} + +/* Connect to a Redis instance. On error the field error in the returned + * context will be set to the return value of the error function. + * When no set of reply functions is given, the default set will be used. */ +redisContext *redisConnect(const char *ip, int port) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags |= REDIS_BLOCK; + redisContextConnectTcp(c,ip,port,NULL); + return c; +} + +redisContext *redisConnectWithTimeout(const char *ip, int port, const struct timeval tv) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags |= REDIS_BLOCK; + redisContextConnectTcp(c,ip,port,&tv); + return c; +} + +redisContext *redisConnectNonBlock(const char *ip, int port) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags &= ~REDIS_BLOCK; + redisContextConnectTcp(c,ip,port,NULL); + return c; +} + +redisContext *redisConnectUnix(const char *path) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags |= REDIS_BLOCK; + redisContextConnectUnix(c,path,NULL); + return c; +} + +redisContext *redisConnectUnixWithTimeout(const char *path, const struct timeval tv) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags |= REDIS_BLOCK; + redisContextConnectUnix(c,path,&tv); + return c; +} + +redisContext *redisConnectUnixNonBlock(const char *path) { + redisContext *c; + + c = redisContextInit(); + if (c == NULL) + return NULL; + + c->flags &= ~REDIS_BLOCK; + redisContextConnectUnix(c,path,NULL); + return c; +} + +/* Set read/write timeout on a blocking socket. */ +int redisSetTimeout(redisContext *c, const struct timeval tv) { + if (c->flags & REDIS_BLOCK) + return redisContextSetTimeout(c,tv); + return REDIS_ERR; +} + +/* Enable connection KeepAlive. */ +int redisEnableKeepAlive(redisContext *c) { + if (redisKeepAlive(c, REDIS_KEEPALIVE_INTERVAL) != REDIS_OK) + return REDIS_ERR; + return REDIS_OK; +} + +/* Use this function to handle a read event on the descriptor. It will try + * and read some bytes from the socket and feed them to the reply parser. + * + * After this function is called, you may use redisContextReadReply to + * see if there is a reply available. */ +int redisBufferRead(redisContext *c) { + char buf[1024*16]; + int nread; + + /* Return early when the context has seen an error. */ + if (c->err) + return REDIS_ERR; + + nread = read(c->fd,buf,sizeof(buf)); + if (nread == -1) { + if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { + /* Try again later */ + } else { + __redisSetError(c,REDIS_ERR_IO,NULL); + return REDIS_ERR; + } + } else if (nread == 0) { + __redisSetError(c,REDIS_ERR_EOF,"Server closed the connection"); + return REDIS_ERR; + } else { + if (redisReaderFeed(c->reader,buf,nread) != REDIS_OK) { + __redisSetError(c,c->reader->err,c->reader->errstr); + return REDIS_ERR; + } + } + return REDIS_OK; +} + +/* Write the output buffer to the socket. + * + * Returns REDIS_OK when the buffer is empty, or (a part of) the buffer was + * succesfully written to the socket. When the buffer is empty after the + * write operation, "done" is set to 1 (if given). + * + * Returns REDIS_ERR if an error occured trying to write and sets + * c->errstr to hold the appropriate error string. + */ +int redisBufferWrite(redisContext *c, int *done) { + int nwritten; + + /* Return early when the context has seen an error. */ + if (c->err) + return REDIS_ERR; + + if (sdslen(c->obuf) > 0) { + nwritten = write(c->fd,c->obuf,sdslen(c->obuf)); + if (nwritten == -1) { + if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) { + /* Try again later */ + } else { + __redisSetError(c,REDIS_ERR_IO,NULL); + return REDIS_ERR; + } + } else if (nwritten > 0) { + if (nwritten == (signed)sdslen(c->obuf)) { + sdsfree(c->obuf); + c->obuf = sdsempty(); + } else { + c->obuf = sdsrange(c->obuf,nwritten,-1); + } + } + } + if (done != NULL) *done = (sdslen(c->obuf) == 0); + return REDIS_OK; +} + +/* Internal helper function to try and get a reply from the reader, + * or set an error in the context otherwise. */ +int redisGetReplyFromReader(redisContext *c, void **reply) { + if (redisReaderGetReply(c->reader,reply) == REDIS_ERR) { + __redisSetError(c,c->reader->err,c->reader->errstr); + return REDIS_ERR; + } + return REDIS_OK; +} + +int redisGetReply(redisContext *c, void **reply) { + int wdone = 0; + void *aux = NULL; + + /* Try to read pending replies */ + if (redisGetReplyFromReader(c,&aux) == REDIS_ERR) + return REDIS_ERR; + + /* For the blocking context, flush output buffer and read reply */ + if (aux == NULL && c->flags & REDIS_BLOCK) { + /* Write until done */ + do { + if (redisBufferWrite(c,&wdone) == REDIS_ERR) + return REDIS_ERR; + } while (!wdone); + + /* Read until there is a reply */ + do { + if (redisBufferRead(c) == REDIS_ERR) + return REDIS_ERR; + if (redisGetReplyFromReader(c,&aux) == REDIS_ERR) + return REDIS_ERR; + } while (aux == NULL); + } + + /* Set reply object */ + if (reply != NULL) *reply = aux; + return REDIS_OK; +} + + +/* Helper function for the redisAppendCommand* family of functions. + * + * Write a formatted command to the output buffer. When this family + * is used, you need to call redisGetReply yourself to retrieve + * the reply (or replies in pub/sub). + */ +int __redisAppendCommand(redisContext *c, char *cmd, size_t len) { + sds newbuf; + + newbuf = sdscatlen(c->obuf,cmd,len); + if (newbuf == NULL) { + __redisSetError(c,REDIS_ERR_OOM,"Out of memory"); + return REDIS_ERR; + } + + c->obuf = newbuf; + return REDIS_OK; +} + +int redisvAppendCommand(redisContext *c, const char *format, va_list ap) { + char *cmd; + int len; + + len = redisvFormatCommand(&cmd,format,ap); + if (len == -1) { + __redisSetError(c,REDIS_ERR_OOM,"Out of memory"); + return REDIS_ERR; + } + + if (__redisAppendCommand(c,cmd,len) != REDIS_OK) { + free(cmd); + return REDIS_ERR; + } + + free(cmd); + return REDIS_OK; +} + +int redisAppendCommand(redisContext *c, const char *format, ...) { + va_list ap; + int ret; + + va_start(ap,format); + ret = redisvAppendCommand(c,format,ap); + va_end(ap); + return ret; +} + +int redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen) { + char *cmd; + int len; + + len = redisFormatCommandArgv(&cmd,argc,argv,argvlen); + if (len == -1) { + __redisSetError(c,REDIS_ERR_OOM,"Out of memory"); + return REDIS_ERR; + } + + if (__redisAppendCommand(c,cmd,len) != REDIS_OK) { + free(cmd); + return REDIS_ERR; + } + + free(cmd); + return REDIS_OK; +} + +/* Helper function for the redisCommand* family of functions. + * + * Write a formatted command to the output buffer. If the given context is + * blocking, immediately read the reply into the "reply" pointer. When the + * context is non-blocking, the "reply" pointer will not be used and the + * command is simply appended to the write buffer. + * + * Returns the reply when a reply was succesfully retrieved. Returns NULL + * otherwise. When NULL is returned in a blocking context, the error field + * in the context will be set. + */ +static void *__redisBlockForReply(redisContext *c) { + void *reply; + + if (c->flags & REDIS_BLOCK) { + if (redisGetReply(c,&reply) != REDIS_OK) + return NULL; + return reply; + } + return NULL; +} + +void *redisvCommand(redisContext *c, const char *format, va_list ap) { + if (redisvAppendCommand(c,format,ap) != REDIS_OK) + return NULL; + return __redisBlockForReply(c); +} + +void *redisCommand(redisContext *c, const char *format, ...) { + va_list ap; + void *reply = NULL; + va_start(ap,format); + reply = redisvCommand(c,format,ap); + va_end(ap); + return reply; +} + +void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen) { + if (redisAppendCommandArgv(c,argc,argv,argvlen) != REDIS_OK) + return NULL; + return __redisBlockForReply(c); +} diff --git a/data-server/external/hiredis/hiredis.h b/data-server/external/hiredis/hiredis.h new file mode 100755 index 0000000000..c65098b740 --- /dev/null +++ b/data-server/external/hiredis/hiredis.h @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2009-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; 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. + */ + +#ifndef __HIREDIS_H +#define __HIREDIS_H +#include /* for size_t */ +#include /* for va_list */ +#include /* for struct timeval */ + +#define HIREDIS_MAJOR 0 +#define HIREDIS_MINOR 11 +#define HIREDIS_PATCH 0 + +#define REDIS_ERR -1 +#define REDIS_OK 0 + +/* When an error occurs, the err flag in a context is set to hold the type of + * error that occured. REDIS_ERR_IO means there was an I/O error and you + * should use the "errno" variable to find out what is wrong. + * For other values, the "errstr" field will hold a description. */ +#define REDIS_ERR_IO 1 /* Error in read or write */ +#define REDIS_ERR_EOF 3 /* End of file */ +#define REDIS_ERR_PROTOCOL 4 /* Protocol error */ +#define REDIS_ERR_OOM 5 /* Out of memory */ +#define REDIS_ERR_OTHER 2 /* Everything else... */ + +/* Connection type can be blocking or non-blocking and is set in the + * least significant bit of the flags field in redisContext. */ +#define REDIS_BLOCK 0x1 + +/* Connection may be disconnected before being free'd. The second bit + * in the flags field is set when the context is connected. */ +#define REDIS_CONNECTED 0x2 + +/* The async API might try to disconnect cleanly and flush the output + * buffer and read all subsequent replies before disconnecting. + * This flag means no new commands can come in and the connection + * should be terminated once all replies have been read. */ +#define REDIS_DISCONNECTING 0x4 + +/* Flag specific to the async API which means that the context should be clean + * up as soon as possible. */ +#define REDIS_FREEING 0x8 + +/* Flag that is set when an async callback is executed. */ +#define REDIS_IN_CALLBACK 0x10 + +/* Flag that is set when the async context has one or more subscriptions. */ +#define REDIS_SUBSCRIBED 0x20 + +/* Flag that is set when monitor mode is active */ +#define REDIS_MONITORING 0x40 + +#define REDIS_REPLY_STRING 1 +#define REDIS_REPLY_ARRAY 2 +#define REDIS_REPLY_INTEGER 3 +#define REDIS_REPLY_NIL 4 +#define REDIS_REPLY_STATUS 5 +#define REDIS_REPLY_ERROR 6 + +#define REDIS_READER_MAX_BUF (1024*16) /* Default max unused reader buffer. */ + +#define REDIS_KEEPALIVE_INTERVAL 15 /* seconds */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is the reply object returned by redisCommand() */ +typedef struct redisReply { + int type; /* REDIS_REPLY_* */ + long long integer; /* The integer when type is REDIS_REPLY_INTEGER */ + int len; /* Length of string */ + char *str; /* Used for both REDIS_REPLY_ERROR and REDIS_REPLY_STRING */ + size_t elements; /* number of elements, for REDIS_REPLY_ARRAY */ + struct redisReply **element; /* elements vector for REDIS_REPLY_ARRAY */ +} redisReply; + +typedef struct redisReadTask { + int type; + int elements; /* number of elements in multibulk container */ + int idx; /* index in parent (array) object */ + void *obj; /* holds user-generated value for a read task */ + struct redisReadTask *parent; /* parent task */ + void *privdata; /* user-settable arbitrary field */ +} redisReadTask; + +typedef struct redisReplyObjectFunctions { + void *(*createString)(const redisReadTask*, char*, size_t); + void *(*createArray)(const redisReadTask*, int); + void *(*createInteger)(const redisReadTask*, long long); + void *(*createNil)(const redisReadTask*); + void (*freeObject)(void*); +} redisReplyObjectFunctions; + +/* State for the protocol parser */ +typedef struct redisReader { + int err; /* Error flags, 0 when there is no error */ + char errstr[128]; /* String representation of error when applicable */ + + char *buf; /* Read buffer */ + size_t pos; /* Buffer cursor */ + size_t len; /* Buffer length */ + size_t maxbuf; /* Max length of unused buffer */ + + redisReadTask rstack[9]; + int ridx; /* Index of current read task */ + void *reply; /* Temporary reply pointer */ + + redisReplyObjectFunctions *fn; + void *privdata; +} redisReader; + +/* Public API for the protocol parser. */ +redisReader *redisReaderCreate(void); +void redisReaderFree(redisReader *r); +int redisReaderFeed(redisReader *r, const char *buf, size_t len); +int redisReaderGetReply(redisReader *r, void **reply); + +/* Backwards compatibility, can be removed on big version bump. */ +#define redisReplyReaderCreate redisReaderCreate +#define redisReplyReaderFree redisReaderFree +#define redisReplyReaderFeed redisReaderFeed +#define redisReplyReaderGetReply redisReaderGetReply +#define redisReplyReaderSetPrivdata(_r, _p) (int)(((redisReader*)(_r))->privdata = (_p)) +#define redisReplyReaderGetObject(_r) (((redisReader*)(_r))->reply) +#define redisReplyReaderGetError(_r) (((redisReader*)(_r))->errstr) + +/* Function to free the reply objects hiredis returns by default. */ +void freeReplyObject(void *reply); + +/* Functions to format a command according to the protocol. */ +int redisvFormatCommand(char **target, const char *format, va_list ap); +int redisFormatCommand(char **target, const char *format, ...); +int redisFormatCommandArgv(char **target, int argc, const char **argv, const size_t *argvlen); + +/* Context for a connection to Redis */ +typedef struct redisContext { + int err; /* Error flags, 0 when there is no error */ + char errstr[128]; /* String representation of error when applicable */ + int fd; + int flags; + char *obuf; /* Write buffer */ + redisReader *reader; /* Protocol reader */ +} redisContext; + +redisContext *redisConnect(const char *ip, int port); +redisContext *redisConnectWithTimeout(const char *ip, int port, const struct timeval tv); +redisContext *redisConnectNonBlock(const char *ip, int port); +redisContext *redisConnectUnix(const char *path); +redisContext *redisConnectUnixWithTimeout(const char *path, const struct timeval tv); +redisContext *redisConnectUnixNonBlock(const char *path); +int redisSetTimeout(redisContext *c, const struct timeval tv); +int redisEnableKeepAlive(redisContext *c); +void redisFree(redisContext *c); +int redisBufferRead(redisContext *c); +int redisBufferWrite(redisContext *c, int *done); + +/* In a blocking context, this function first checks if there are unconsumed + * replies to return and returns one if so. Otherwise, it flushes the output + * buffer to the socket and reads until it has a reply. In a non-blocking + * context, it will return unconsumed replies until there are no more. */ +int redisGetReply(redisContext *c, void **reply); +int redisGetReplyFromReader(redisContext *c, void **reply); + +/* Write a command to the output buffer. Use these functions in blocking mode + * to get a pipeline of commands. */ +int redisvAppendCommand(redisContext *c, const char *format, va_list ap); +int redisAppendCommand(redisContext *c, const char *format, ...); +int redisAppendCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); + +/* Issue a command to Redis. In a blocking context, it is identical to calling + * redisAppendCommand, followed by redisGetReply. The function will return + * NULL if there was an error in performing the request, otherwise it will + * return the reply. In a non-blocking context, it is identical to calling + * only redisAppendCommand and will always return NULL. */ +void *redisvCommand(redisContext *c, const char *format, va_list ap); +void *redisCommand(redisContext *c, const char *format, ...); +void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/data-server/external/hiredis/net.c b/data-server/external/hiredis/net.c new file mode 100755 index 0000000000..603699c936 --- /dev/null +++ b/data-server/external/hiredis/net.c @@ -0,0 +1,339 @@ +/* Extracted from anet.c to work properly with Hiredis error reporting. + * + * Copyright (c) 2006-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; 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. + */ + +#include "fmacros.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "net.h" +#include "sds.h" + +/* Defined in hiredis.c */ +void __redisSetError(redisContext *c, int type, const char *str); + +static void __redisSetErrorFromErrno(redisContext *c, int type, const char *prefix) { + char buf[128] = { 0 }; + size_t len = 0; + + if (prefix != NULL) + len = snprintf(buf,sizeof(buf),"%s: ",prefix); + strerror_r(errno,buf+len,sizeof(buf)-len); + __redisSetError(c,type,buf); +} + +static int redisSetReuseAddr(redisContext *c, int fd) { + int on = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); + close(fd); + return REDIS_ERR; + } + return REDIS_OK; +} + +static int redisCreateSocket(redisContext *c, int type) { + int s; + if ((s = socket(type, SOCK_STREAM, 0)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); + return REDIS_ERR; + } + if (type == AF_INET) { + if (redisSetReuseAddr(c,s) == REDIS_ERR) { + return REDIS_ERR; + } + } + return s; +} + +static int redisSetBlocking(redisContext *c, int fd, int blocking) { + int flags; + + /* Set the socket nonblocking. + * Note that fcntl(2) for F_GETFL and F_SETFL can't be + * interrupted by a signal. */ + if ((flags = fcntl(fd, F_GETFL)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"fcntl(F_GETFL)"); + close(fd); + return REDIS_ERR; + } + + if (blocking) + flags &= ~O_NONBLOCK; + else + flags |= O_NONBLOCK; + + if (fcntl(fd, F_SETFL, flags) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"fcntl(F_SETFL)"); + close(fd); + return REDIS_ERR; + } + return REDIS_OK; +} + +int redisKeepAlive(redisContext *c, int interval) { + int val = 1; + int fd = c->fd; + + if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof(val)) == -1){ + __redisSetError(c,REDIS_ERR_OTHER,strerror(errno)); + return REDIS_ERR; + } + +#ifdef _OSX + val = interval; + if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPALIVE, &val, sizeof(val)) < 0) { + __redisSetError(c,REDIS_ERR_OTHER,strerror(errno)); + return REDIS_ERR; + } +#else + val = interval; + if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &val, sizeof(val)) < 0) { + __redisSetError(c,REDIS_ERR_OTHER,strerror(errno)); + return REDIS_ERR; + } + + val = interval/3; + if (val == 0) val = 1; + if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &val, sizeof(val)) < 0) { + __redisSetError(c,REDIS_ERR_OTHER,strerror(errno)); + return REDIS_ERR; + } + + val = 3; + if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &val, sizeof(val)) < 0) { + __redisSetError(c,REDIS_ERR_OTHER,strerror(errno)); + return REDIS_ERR; + } +#endif + + return REDIS_OK; +} + +static int redisSetTcpNoDelay(redisContext *c, int fd) { + int yes = 1; + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(TCP_NODELAY)"); + close(fd); + return REDIS_ERR; + } + return REDIS_OK; +} + +#define __MAX_MSEC (((LONG_MAX) - 999) / 1000) + +static int redisContextWaitReady(redisContext *c, int fd, const struct timeval *timeout) { + struct pollfd wfd[1]; + long msec; + + msec = -1; + wfd[0].fd = fd; + wfd[0].events = POLLOUT; + + /* Only use timeout when not NULL. */ + if (timeout != NULL) { + if (timeout->tv_usec > 1000000 || timeout->tv_sec > __MAX_MSEC) { + __redisSetErrorFromErrno(c, REDIS_ERR_IO, NULL); + close(fd); + return REDIS_ERR; + } + + msec = (timeout->tv_sec * 1000) + ((timeout->tv_usec + 999) / 1000); + + if (msec < 0 || msec > INT_MAX) { + msec = INT_MAX; + } + } + + if (errno == EINPROGRESS) { + int res; + + if ((res = poll(wfd, 1, msec)) == -1) { + __redisSetErrorFromErrno(c, REDIS_ERR_IO, "poll(2)"); + close(fd); + return REDIS_ERR; + } else if (res == 0) { + errno = ETIMEDOUT; + __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); + close(fd); + return REDIS_ERR; + } + + if (redisCheckSocketError(c, fd) != REDIS_OK) + return REDIS_ERR; + + return REDIS_OK; + } + + __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); + close(fd); + return REDIS_ERR; +} + +int redisCheckSocketError(redisContext *c, int fd) { + int err = 0; + socklen_t errlen = sizeof(err); + + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"getsockopt(SO_ERROR)"); + close(fd); + return REDIS_ERR; + } + + if (err) { + errno = err; + __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL); + close(fd); + return REDIS_ERR; + } + + return REDIS_OK; +} + +int redisContextSetTimeout(redisContext *c, const struct timeval tv) { + if (setsockopt(c->fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(SO_RCVTIMEO)"); + return REDIS_ERR; + } + if (setsockopt(c->fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv)) == -1) { + __redisSetErrorFromErrno(c,REDIS_ERR_IO,"setsockopt(SO_SNDTIMEO)"); + return REDIS_ERR; + } + return REDIS_OK; +} + +int redisContextConnectTcp(redisContext *c, const char *addr, int port, const struct timeval *timeout) { + int s, rv; + char _port[6]; /* strlen("65535"); */ + struct addrinfo hints, *servinfo, *p; + int blocking = (c->flags & REDIS_BLOCK); + + snprintf(_port, 6, "%d", port); + memset(&hints,0,sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + /* Try with IPv6 if no IPv4 address was found. We do it in this order since + * in a Redis client you can't afford to test if you have IPv6 connectivity + * as this would add latency to every connect. Otherwise a more sensible + * route could be: Use IPv6 if both addresses are available and there is IPv6 + * connectivity. */ + if ((rv = getaddrinfo(addr,_port,&hints,&servinfo)) != 0) { + hints.ai_family = AF_INET6; + if ((rv = getaddrinfo(addr,_port,&hints,&servinfo)) != 0) { + __redisSetError(c,REDIS_ERR_OTHER,gai_strerror(rv)); + return REDIS_ERR; + } + } + for (p = servinfo; p != NULL; p = p->ai_next) { + if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1) + continue; + + if (redisSetBlocking(c,s,0) != REDIS_OK) + goto error; + if (connect(s,p->ai_addr,p->ai_addrlen) == -1) { + if (errno == EHOSTUNREACH) { + close(s); + continue; + } else if (errno == EINPROGRESS && !blocking) { + /* This is ok. */ + } else { + if (redisContextWaitReady(c,s,timeout) != REDIS_OK) + goto error; + } + } + if (blocking && redisSetBlocking(c,s,1) != REDIS_OK) + goto error; + if (redisSetTcpNoDelay(c,s) != REDIS_OK) + goto error; + + c->fd = s; + c->flags |= REDIS_CONNECTED; + rv = REDIS_OK; + goto end; + } + if (p == NULL) { + char buf[128]; + snprintf(buf,sizeof(buf),"Can't create socket: %s",strerror(errno)); + __redisSetError(c,REDIS_ERR_OTHER,buf); + goto error; + } + +error: + rv = REDIS_ERR; +end: + freeaddrinfo(servinfo); + return rv; // Need to return REDIS_OK if alright +} + +int redisContextConnectUnix(redisContext *c, const char *path, const struct timeval *timeout) { + int s; + int blocking = (c->flags & REDIS_BLOCK); + struct sockaddr_un sa; + + if ((s = redisCreateSocket(c,AF_LOCAL)) < 0) + return REDIS_ERR; + if (redisSetBlocking(c,s,0) != REDIS_OK) + return REDIS_ERR; + + sa.sun_family = AF_LOCAL; + strncpy(sa.sun_path,path,sizeof(sa.sun_path)-1); + if (connect(s, (struct sockaddr*)&sa, sizeof(sa)) == -1) { + if (errno == EINPROGRESS && !blocking) { + /* This is ok. */ + } else { + if (redisContextWaitReady(c,s,timeout) != REDIS_OK) + return REDIS_ERR; + } + } + + /* Reset socket to be blocking after connect(2). */ + if (blocking && redisSetBlocking(c,s,1) != REDIS_OK) + return REDIS_ERR; + + c->fd = s; + c->flags |= REDIS_CONNECTED; + return REDIS_OK; +} diff --git a/data-server/external/hiredis/net.h b/data-server/external/hiredis/net.h new file mode 100755 index 0000000000..94b76f5ca7 --- /dev/null +++ b/data-server/external/hiredis/net.h @@ -0,0 +1,48 @@ +/* Extracted from anet.c to work properly with Hiredis error reporting. + * + * Copyright (c) 2006-2011, Salvatore Sanfilippo + * Copyright (c) 2010-2011, Pieter Noordhuis + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; 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. + */ + +#ifndef __NET_H +#define __NET_H + +#include "hiredis.h" + +#if defined(__sun) +#define AF_LOCAL AF_UNIX +#endif + +int redisCheckSocketError(redisContext *c, int fd); +int redisContextSetTimeout(redisContext *c, const struct timeval tv); +int redisContextConnectTcp(redisContext *c, const char *addr, int port, const struct timeval *timeout); +int redisContextConnectUnix(redisContext *c, const char *path, const struct timeval *timeout); +int redisKeepAlive(redisContext *c, int interval); + +#endif diff --git a/data-server/external/hiredis/sds.c b/data-server/external/hiredis/sds.c new file mode 100755 index 0000000000..9226799fcf --- /dev/null +++ b/data-server/external/hiredis/sds.c @@ -0,0 +1,606 @@ +/* SDSLib, A C dynamic strings library + * + * Copyright (c) 2006-2010, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; 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. + */ + +#include +#include +#include +#include +#include "sds.h" + +#ifdef SDS_ABORT_ON_OOM +static void sdsOomAbort(void) { + fprintf(stderr,"SDS: Out Of Memory (SDS_ABORT_ON_OOM defined)\n"); + abort(); +} +#endif + +sds sdsnewlen(const void *init, size_t initlen) { + struct sdshdr *sh; + + sh = malloc(sizeof(struct sdshdr)+initlen+1); +#ifdef SDS_ABORT_ON_OOM + if (sh == NULL) sdsOomAbort(); +#else + if (sh == NULL) return NULL; +#endif + sh->len = initlen; + sh->free = 0; + if (initlen) { + if (init) memcpy(sh->buf, init, initlen); + else memset(sh->buf,0,initlen); + } + sh->buf[initlen] = '\0'; + return (char*)sh->buf; +} + +sds sdsempty(void) { + return sdsnewlen("",0); +} + +sds sdsnew(const char *init) { + size_t initlen = (init == NULL) ? 0 : strlen(init); + return sdsnewlen(init, initlen); +} + +sds sdsdup(const sds s) { + return sdsnewlen(s, sdslen(s)); +} + +void sdsfree(sds s) { + if (s == NULL) return; + free(s-sizeof(struct sdshdr)); +} + +void sdsupdatelen(sds s) { + struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); + int reallen = strlen(s); + sh->free += (sh->len-reallen); + sh->len = reallen; +} + +static sds sdsMakeRoomFor(sds s, size_t addlen) { + struct sdshdr *sh, *newsh; + size_t free = sdsavail(s); + size_t len, newlen; + + if (free >= addlen) return s; + len = sdslen(s); + sh = (void*) (s-(sizeof(struct sdshdr))); + newlen = (len+addlen)*2; + newsh = realloc(sh, sizeof(struct sdshdr)+newlen+1); +#ifdef SDS_ABORT_ON_OOM + if (newsh == NULL) sdsOomAbort(); +#else + if (newsh == NULL) return NULL; +#endif + + newsh->free = newlen - len; + return newsh->buf; +} + +/* Grow the sds to have the specified length. Bytes that were not part of + * the original length of the sds will be set to zero. */ +sds sdsgrowzero(sds s, size_t len) { + struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); + size_t totlen, curlen = sh->len; + + if (len <= curlen) return s; + s = sdsMakeRoomFor(s,len-curlen); + if (s == NULL) return NULL; + + /* Make sure added region doesn't contain garbage */ + sh = (void*)(s-(sizeof(struct sdshdr))); + memset(s+curlen,0,(len-curlen+1)); /* also set trailing \0 byte */ + totlen = sh->len+sh->free; + sh->len = len; + sh->free = totlen-sh->len; + return s; +} + +sds sdscatlen(sds s, const void *t, size_t len) { + struct sdshdr *sh; + size_t curlen = sdslen(s); + + s = sdsMakeRoomFor(s,len); + if (s == NULL) return NULL; + sh = (void*) (s-(sizeof(struct sdshdr))); + memcpy(s+curlen, t, len); + sh->len = curlen+len; + sh->free = sh->free-len; + s[curlen+len] = '\0'; + return s; +} + +sds sdscat(sds s, const char *t) { + return sdscatlen(s, t, strlen(t)); +} + +sds sdscpylen(sds s, char *t, size_t len) { + struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); + size_t totlen = sh->free+sh->len; + + if (totlen < len) { + s = sdsMakeRoomFor(s,len-sh->len); + if (s == NULL) return NULL; + sh = (void*) (s-(sizeof(struct sdshdr))); + totlen = sh->free+sh->len; + } + memcpy(s, t, len); + s[len] = '\0'; + sh->len = len; + sh->free = totlen-len; + return s; +} + +sds sdscpy(sds s, char *t) { + return sdscpylen(s, t, strlen(t)); +} + +sds sdscatvprintf(sds s, const char *fmt, va_list ap) { + va_list cpy; + char *buf, *t; + size_t buflen = 16; + + while(1) { + buf = malloc(buflen); +#ifdef SDS_ABORT_ON_OOM + if (buf == NULL) sdsOomAbort(); +#else + if (buf == NULL) return NULL; +#endif + buf[buflen-2] = '\0'; + va_copy(cpy,ap); + vsnprintf(buf, buflen, fmt, cpy); + va_end(cpy); + if (buf[buflen-2] != '\0') { + free(buf); + buflen *= 2; + continue; + } + break; + } + t = sdscat(s, buf); + free(buf); + return t; +} + +sds sdscatprintf(sds s, const char *fmt, ...) { + va_list ap; + char *t; + va_start(ap, fmt); + t = sdscatvprintf(s,fmt,ap); + va_end(ap); + return t; +} + +sds sdstrim(sds s, const char *cset) { + struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); + char *start, *end, *sp, *ep; + size_t len; + + sp = start = s; + ep = end = s+sdslen(s)-1; + while(sp <= end && strchr(cset, *sp)) sp++; + while(ep > start && strchr(cset, *ep)) ep--; + len = (sp > ep) ? 0 : ((ep-sp)+1); + if (sh->buf != sp) memmove(sh->buf, sp, len); + sh->buf[len] = '\0'; + sh->free = sh->free+(sh->len-len); + sh->len = len; + return s; +} + +sds sdsrange(sds s, int start, int end) { + struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr))); + size_t newlen, len = sdslen(s); + + if (len == 0) return s; + if (start < 0) { + start = len+start; + if (start < 0) start = 0; + } + if (end < 0) { + end = len+end; + if (end < 0) end = 0; + } + newlen = (start > end) ? 0 : (end-start)+1; + if (newlen != 0) { + if (start >= (signed)len) { + newlen = 0; + } else if (end >= (signed)len) { + end = len-1; + newlen = (start > end) ? 0 : (end-start)+1; + } + } else { + start = 0; + } + if (start && newlen) memmove(sh->buf, sh->buf+start, newlen); + sh->buf[newlen] = 0; + sh->free = sh->free+(sh->len-newlen); + sh->len = newlen; + return s; +} + +void sdstolower(sds s) { + int len = sdslen(s), j; + + for (j = 0; j < len; j++) s[j] = tolower(s[j]); +} + +void sdstoupper(sds s) { + int len = sdslen(s), j; + + for (j = 0; j < len; j++) s[j] = toupper(s[j]); +} + +int sdscmp(sds s1, sds s2) { + size_t l1, l2, minlen; + int cmp; + + l1 = sdslen(s1); + l2 = sdslen(s2); + minlen = (l1 < l2) ? l1 : l2; + cmp = memcmp(s1,s2,minlen); + if (cmp == 0) return l1-l2; + return cmp; +} + +/* Split 's' with separator in 'sep'. An array + * of sds strings is returned. *count will be set + * by reference to the number of tokens returned. + * + * On out of memory, zero length string, zero length + * separator, NULL is returned. + * + * Note that 'sep' is able to split a string using + * a multi-character separator. For example + * sdssplit("foo_-_bar","_-_"); will return two + * elements "foo" and "bar". + * + * This version of the function is binary-safe but + * requires length arguments. sdssplit() is just the + * same function but for zero-terminated strings. + */ +sds *sdssplitlen(char *s, int len, char *sep, int seplen, int *count) { + int elements = 0, slots = 5, start = 0, j; + + sds *tokens = malloc(sizeof(sds)*slots); +#ifdef SDS_ABORT_ON_OOM + if (tokens == NULL) sdsOomAbort(); +#endif + if (seplen < 1 || len < 0 || tokens == NULL) return NULL; + if (len == 0) { + *count = 0; + return tokens; + } + for (j = 0; j < (len-(seplen-1)); j++) { + /* make sure there is room for the next element and the final one */ + if (slots < elements+2) { + sds *newtokens; + + slots *= 2; + newtokens = realloc(tokens,sizeof(sds)*slots); + if (newtokens == NULL) { +#ifdef SDS_ABORT_ON_OOM + sdsOomAbort(); +#else + goto cleanup; +#endif + } + tokens = newtokens; + } + /* search the separator */ + if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) { + tokens[elements] = sdsnewlen(s+start,j-start); + if (tokens[elements] == NULL) { +#ifdef SDS_ABORT_ON_OOM + sdsOomAbort(); +#else + goto cleanup; +#endif + } + elements++; + start = j+seplen; + j = j+seplen-1; /* skip the separator */ + } + } + /* Add the final element. We are sure there is room in the tokens array. */ + tokens[elements] = sdsnewlen(s+start,len-start); + if (tokens[elements] == NULL) { +#ifdef SDS_ABORT_ON_OOM + sdsOomAbort(); +#else + goto cleanup; +#endif + } + elements++; + *count = elements; + return tokens; + +#ifndef SDS_ABORT_ON_OOM +cleanup: + { + int i; + for (i = 0; i < elements; i++) sdsfree(tokens[i]); + free(tokens); + return NULL; + } +#endif +} + +void sdsfreesplitres(sds *tokens, int count) { + if (!tokens) return; + while(count--) + sdsfree(tokens[count]); + free(tokens); +} + +sds sdsfromlonglong(long long value) { + char buf[32], *p; + unsigned long long v; + + v = (value < 0) ? -value : value; + p = buf+31; /* point to the last character */ + do { + *p-- = '0'+(v%10); + v /= 10; + } while(v); + if (value < 0) *p-- = '-'; + p++; + return sdsnewlen(p,32-(p-buf)); +} + +sds sdscatrepr(sds s, char *p, size_t len) { + s = sdscatlen(s,"\"",1); + if (s == NULL) return NULL; + + while(len--) { + switch(*p) { + case '\\': + case '"': + s = sdscatprintf(s,"\\%c",*p); + break; + case '\n': s = sdscatlen(s,"\\n",2); break; + case '\r': s = sdscatlen(s,"\\r",2); break; + case '\t': s = sdscatlen(s,"\\t",2); break; + case '\a': s = sdscatlen(s,"\\a",2); break; + case '\b': s = sdscatlen(s,"\\b",2); break; + default: + if (isprint(*p)) + s = sdscatprintf(s,"%c",*p); + else + s = sdscatprintf(s,"\\x%02x",(unsigned char)*p); + break; + } + p++; + if (s == NULL) return NULL; + } + return sdscatlen(s,"\"",1); +} + +/* Split a line into arguments, where every argument can be in the + * following programming-language REPL-alike form: + * + * foo bar "newline are supported\n" and "\xff\x00otherstuff" + * + * The number of arguments is stored into *argc, and an array + * of sds is returned. The caller should sdsfree() all the returned + * strings and finally free() the array itself. + * + * Note that sdscatrepr() is able to convert back a string into + * a quoted string in the same format sdssplitargs() is able to parse. + */ +sds *sdssplitargs(char *line, int *argc) { + char *p = line; + char *current = NULL; + char **vector = NULL, **_vector = NULL; + + *argc = 0; + while(1) { + /* skip blanks */ + while(*p && isspace(*p)) p++; + if (*p) { + /* get a token */ + int inq=0; /* set to 1 if we are in "quotes" */ + int done=0; + + if (current == NULL) { + current = sdsempty(); + if (current == NULL) goto err; + } + + while(!done) { + if (inq) { + if (*p == '\\' && *(p+1)) { + char c; + + p++; + switch(*p) { + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'b': c = '\b'; break; + case 'a': c = '\a'; break; + default: c = *p; break; + } + current = sdscatlen(current,&c,1); + } else if (*p == '"') { + /* closing quote must be followed by a space */ + if (*(p+1) && !isspace(*(p+1))) goto err; + done=1; + } else if (!*p) { + /* unterminated quotes */ + goto err; + } else { + current = sdscatlen(current,p,1); + } + } else { + switch(*p) { + case ' ': + case '\n': + case '\r': + case '\t': + case '\0': + done=1; + break; + case '"': + inq=1; + break; + default: + current = sdscatlen(current,p,1); + break; + } + } + if (*p) p++; + if (current == NULL) goto err; + } + /* add the token to the vector */ + _vector = realloc(vector,((*argc)+1)*sizeof(char*)); + if (_vector == NULL) goto err; + + vector = _vector; + vector[*argc] = current; + (*argc)++; + current = NULL; + } else { + return vector; + } + } + +err: + while((*argc)--) + sdsfree(vector[*argc]); + if (vector != NULL) free(vector); + if (current != NULL) sdsfree(current); + return NULL; +} + +#ifdef SDS_TEST_MAIN +#include + +int __failed_tests = 0; +int __test_num = 0; +#define test_cond(descr,_c) do { \ + __test_num++; printf("%d - %s: ", __test_num, descr); \ + if(_c) printf("PASSED\n"); else {printf("FAILED\n"); __failed_tests++;} \ +} while(0); +#define test_report() do { \ + printf("%d tests, %d passed, %d failed\n", __test_num, \ + __test_num-__failed_tests, __failed_tests); \ + if (__failed_tests) { \ + printf("=== WARNING === We have failed tests here...\n"); \ + } \ +} while(0); + +int main(void) { + { + sds x = sdsnew("foo"), y; + + test_cond("Create a string and obtain the length", + sdslen(x) == 3 && memcmp(x,"foo\0",4) == 0) + + sdsfree(x); + x = sdsnewlen("foo",2); + test_cond("Create a string with specified length", + sdslen(x) == 2 && memcmp(x,"fo\0",3) == 0) + + x = sdscat(x,"bar"); + test_cond("Strings concatenation", + sdslen(x) == 5 && memcmp(x,"fobar\0",6) == 0); + + x = sdscpy(x,"a"); + test_cond("sdscpy() against an originally longer string", + sdslen(x) == 1 && memcmp(x,"a\0",2) == 0) + + x = sdscpy(x,"xyzxxxxxxxxxxyyyyyyyyyykkkkkkkkkk"); + test_cond("sdscpy() against an originally shorter string", + sdslen(x) == 33 && + memcmp(x,"xyzxxxxxxxxxxyyyyyyyyyykkkkkkkkkk\0",33) == 0) + + sdsfree(x); + x = sdscatprintf(sdsempty(),"%d",123); + test_cond("sdscatprintf() seems working in the base case", + sdslen(x) == 3 && memcmp(x,"123\0",4) ==0) + + sdsfree(x); + x = sdstrim(sdsnew("xxciaoyyy"),"xy"); + test_cond("sdstrim() correctly trims characters", + sdslen(x) == 4 && memcmp(x,"ciao\0",5) == 0) + + y = sdsrange(sdsdup(x),1,1); + test_cond("sdsrange(...,1,1)", + sdslen(y) == 1 && memcmp(y,"i\0",2) == 0) + + sdsfree(y); + y = sdsrange(sdsdup(x),1,-1); + test_cond("sdsrange(...,1,-1)", + sdslen(y) == 3 && memcmp(y,"iao\0",4) == 0) + + sdsfree(y); + y = sdsrange(sdsdup(x),-2,-1); + test_cond("sdsrange(...,-2,-1)", + sdslen(y) == 2 && memcmp(y,"ao\0",3) == 0) + + sdsfree(y); + y = sdsrange(sdsdup(x),2,1); + test_cond("sdsrange(...,2,1)", + sdslen(y) == 0 && memcmp(y,"\0",1) == 0) + + sdsfree(y); + y = sdsrange(sdsdup(x),1,100); + test_cond("sdsrange(...,1,100)", + sdslen(y) == 3 && memcmp(y,"iao\0",4) == 0) + + sdsfree(y); + y = sdsrange(sdsdup(x),100,100); + test_cond("sdsrange(...,100,100)", + sdslen(y) == 0 && memcmp(y,"\0",1) == 0) + + sdsfree(y); + sdsfree(x); + x = sdsnew("foo"); + y = sdsnew("foa"); + test_cond("sdscmp(foo,foa)", sdscmp(x,y) > 0) + + sdsfree(y); + sdsfree(x); + x = sdsnew("bar"); + y = sdsnew("bar"); + test_cond("sdscmp(bar,bar)", sdscmp(x,y) == 0) + + sdsfree(y); + sdsfree(x); + x = sdsnew("aar"); + y = sdsnew("bar"); + test_cond("sdscmp(bar,bar)", sdscmp(x,y) < 0) + } + test_report() +} +#endif diff --git a/data-server/external/hiredis/sds.h b/data-server/external/hiredis/sds.h new file mode 100755 index 0000000000..94f5871f54 --- /dev/null +++ b/data-server/external/hiredis/sds.h @@ -0,0 +1,88 @@ +/* SDSLib, A C dynamic strings library + * + * Copyright (c) 2006-2010, Salvatore Sanfilippo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Redis nor the names of its contributors may be used + * to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; 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. + */ + +#ifndef __SDS_H +#define __SDS_H + +#include +#include + +typedef char *sds; + +struct sdshdr { + int len; + int free; + char buf[]; +}; + +static inline size_t sdslen(const sds s) { + struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); + return sh->len; +} + +static inline size_t sdsavail(const sds s) { + struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); + return sh->free; +} + +sds sdsnewlen(const void *init, size_t initlen); +sds sdsnew(const char *init); +sds sdsempty(void); +size_t sdslen(const sds s); +sds sdsdup(const sds s); +void sdsfree(sds s); +size_t sdsavail(sds s); +sds sdsgrowzero(sds s, size_t len); +sds sdscatlen(sds s, const void *t, size_t len); +sds sdscat(sds s, const char *t); +sds sdscpylen(sds s, char *t, size_t len); +sds sdscpy(sds s, char *t); + +sds sdscatvprintf(sds s, const char *fmt, va_list ap); +#ifdef __GNUC__ +sds sdscatprintf(sds s, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +#else +sds sdscatprintf(sds s, const char *fmt, ...); +#endif + +sds sdstrim(sds s, const char *cset); +sds sdsrange(sds s, int start, int end); +void sdsupdatelen(sds s); +int sdscmp(sds s1, sds s2); +sds *sdssplitlen(char *s, int len, char *sep, int seplen, int *count); +void sdsfreesplitres(sds *tokens, int count); +void sdstolower(sds s); +void sdstoupper(sds s); +sds sdsfromlonglong(long long value); +sds sdscatrepr(sds s, char *p, size_t len); +sds *sdssplitargs(char *line, int *argc); + +#endif diff --git a/data-server/src/DataServer.cpp b/data-server/src/DataServer.cpp new file mode 100644 index 0000000000..2916403c97 --- /dev/null +++ b/data-server/src/DataServer.cpp @@ -0,0 +1,200 @@ +// +// DataServer.cpp +// hifi +// +// Created by Stephen Birarda on 1/20/2014. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// + +#include + +#include +#include + +#include "DataServer.h" + +const quint16 DATA_SERVER_LISTEN_PORT = 3282; + +const char REDIS_HOSTNAME[] = "127.0.0.1"; +const unsigned short REDIS_PORT = 6379; + +DataServer::DataServer(int argc, char* argv[]) : + QCoreApplication(argc, argv), + _socket(), + _redis(NULL) +{ + _socket.bind(QHostAddress::Any, DATA_SERVER_LISTEN_PORT); + + connect(&_socket, &QUdpSocket::readyRead, this, &DataServer::readPendingDatagrams); + + _redis = redisConnect(REDIS_HOSTNAME, REDIS_PORT); + + if (_redis && _redis->err) { + if (_redis) { + qDebug() << "Redis connection error:" << _redis->errstr; + } else { + qDebug("Redis connection error - can't allocate redis context."); + } + + // couldn't connect to redis, bail out + QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); + } +} + +DataServer::~DataServer() { + // disconnect from redis and free the context + if (_redis) { + redisFree(_redis); + } +} + +const int MAX_PACKET_SIZE = 1500; + +void DataServer::readPendingDatagrams() { + qint64 receivedBytes = 0; + static unsigned char packetData[MAX_PACKET_SIZE]; + + QHostAddress senderAddress; + quint16 senderPort; + + while (_socket.hasPendingDatagrams() && + (receivedBytes = _socket.readDatagram(reinterpret_cast(packetData), MAX_PACKET_SIZE, + &senderAddress, &senderPort))) { + if ((packetData[0] == PACKET_TYPE_DATA_SERVER_PUT || packetData[0] == PACKET_TYPE_DATA_SERVER_GET) && + packetVersionMatch(packetData)) { + + int readBytes = numBytesForPacketHeader(packetData); + + // pull the sequence number used for this packet + quint8 sequenceNumber = 0; + memcpy(&sequenceNumber, packetData + readBytes, sizeof(sequenceNumber)); + readBytes += sizeof(sequenceNumber); + + // pull the UUID that we will need as part of the key + QString uuidString(reinterpret_cast(packetData + readBytes)); + QUuid parsedUUID(uuidString); + + if (parsedUUID.isNull()) { + // we failed to parse a UUID, this means the user has sent us a username + + QString username(reinterpret_cast(packetData + readBytes)); + readBytes += username.size() + sizeof('\0'); + + // ask redis for the UUID for this user + redisReply* reply = (redisReply*) redisCommand(_redis, "GET user:%s", qPrintable(username)); + + if (reply->type == REDIS_REPLY_STRING) { + parsedUUID = QUuid(QString(reply->str)); + } + + if (!parsedUUID.isNull()) { + qDebug() << "Found UUID" << parsedUUID << "for username" << username; + } else { + qDebug() << "Failed UUID lookup for username" << username; + } + + freeReplyObject(reply); + reply = NULL; + } else { + readBytes += uuidString.size() + sizeof('\0'); + } + + if (!parsedUUID.isNull()) { + // pull the number of keys the user has sent + unsigned char numKeys = packetData[readBytes++]; + + if (packetData[0] == PACKET_TYPE_DATA_SERVER_PUT) { + + // pull the key that specifies the data the user is putting/getting + QString dataKey(reinterpret_cast(packetData + readBytes)); + readBytes += dataKey.size() + sizeof('\0'); + + // grab the string value the user wants us to put, null terminate it + QString dataValue(reinterpret_cast(packetData + readBytes)); + readBytes += dataValue.size() + sizeof('\0'); + + qDebug("Sending command to redis: SET uuid:%s:%s %s", + qPrintable(uuidStringWithoutCurlyBraces(parsedUUID)), + qPrintable(dataKey), qPrintable(dataValue)); + + redisReply* reply = (redisReply*) redisCommand(_redis, "SET uuid:%s:%s %s", + qPrintable(uuidStringWithoutCurlyBraces(parsedUUID)), + qPrintable(dataKey), qPrintable(dataValue)); + + if (reply->type == REDIS_REPLY_STATUS && strcmp("OK", reply->str) == 0) { + // if redis stored the value successfully reply back with a confirm + // which is the sent packet with the header replaced + packetData[0] = PACKET_TYPE_DATA_SERVER_CONFIRM; + _socket.writeDatagram(reinterpret_cast(packetData), receivedBytes, + senderAddress, senderPort); + } + + freeReplyObject(reply); + } else { + // setup a send packet with the returned data + // leverage the packetData sent by overwriting and appending + int numSendPacketBytes = receivedBytes; + + packetData[0] = PACKET_TYPE_DATA_SERVER_SEND; + + const char MULTI_KEY_VALUE_SEPARATOR = '|'; + + if (strcmp((char*) packetData + readBytes, "uuid") != 0) { + + // the user has sent one or more keys - make the associated requests + for (int j = 0; j < numKeys; j++) { + + // pull the key that specifies the data the user is putting/getting, null terminate it + int numDataKeyBytes = 0; + + // look for the key separator or the null terminator + while (packetData[readBytes + numDataKeyBytes] != MULTI_KEY_VALUE_SEPARATOR + && packetData[readBytes + numDataKeyBytes] != '\0') { + numDataKeyBytes++; + } + + QString dataKey(QByteArray(reinterpret_cast(packetData + readBytes), numDataKeyBytes)); + readBytes += dataKey.size() + sizeof('\0'); + + qDebug("Sending command to redis: GET uuid:%s:%s", + qPrintable(uuidStringWithoutCurlyBraces(parsedUUID)), + qPrintable(dataKey)); + redisReply* reply = (redisReply*) redisCommand(_redis, "GET uuid:%s:%s", + qPrintable(uuidStringWithoutCurlyBraces(parsedUUID)), + qPrintable(dataKey)); + + if (reply->len) { + // copy the value that redis returned + memcpy(packetData + numSendPacketBytes, reply->str, reply->len); + numSendPacketBytes += reply->len; + + } else { + // didn't find a value - insert a space + packetData[numSendPacketBytes++] = ' '; + } + + // add the multi-value separator + packetData[numSendPacketBytes++] = MULTI_KEY_VALUE_SEPARATOR; + + freeReplyObject(reply); + } + + // null terminate the packet we're sending back (erases the trailing separator) + packetData[(numSendPacketBytes - 1)] = '\0'; + } else { + // user is asking for a UUID matching username, copy the UUID we found + QString uuidString = uuidStringWithoutCurlyBraces(parsedUUID); + memcpy(packetData + numSendPacketBytes, uuidString.constData(), uuidString.size() + sizeof('\0')); + numSendPacketBytes += uuidString.size() + sizeof('\0'); + } + + // reply back with the send packet + _socket.writeDatagram(reinterpret_cast(packetData), numSendPacketBytes, + senderAddress, senderPort); + + + } + } + } + } +} \ No newline at end of file diff --git a/data-server/src/DataServer.h b/data-server/src/DataServer.h new file mode 100644 index 0000000000..097f6a6533 --- /dev/null +++ b/data-server/src/DataServer.h @@ -0,0 +1,29 @@ +// +// DataServer.h +// hifi +// +// Created by Stephen Birarda on 1/20/2014. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// + +#ifndef __hifi__DataServer__ +#define __hifi__DataServer__ + +#include +#include + +#include + +class DataServer : public QCoreApplication { + Q_OBJECT +public: + DataServer(int argc, char* argv[]); + ~DataServer(); +private: + QUdpSocket _socket; + redisContext* _redis; +private slots: + void readPendingDatagrams(); +}; + +#endif /* defined(__hifi__DataServer__) */ diff --git a/data-server/src/main.cpp b/data-server/src/main.cpp new file mode 100755 index 0000000000..dc99de70b8 --- /dev/null +++ b/data-server/src/main.cpp @@ -0,0 +1,28 @@ +// +// main.cpp +// data-server +// +// Created by Stephen Birarda on 1/1/13. +// Copyright (c) 2013 HighFidelity, Inc. All rights reserved. +// + +#include + +#include + +#include "DataServer.h" + +const char DATA_SERVER_LOGGING_TARGET_NAME[] = "data-server"; + +int main(int argc, char* argv[]) { + + setvbuf(stdout, NULL, _IOLBF, 0); + + qInstallMessageHandler(Logging::verboseMessageHandler); + + Logging::setTargetName(DATA_SERVER_LOGGING_TARGET_NAME); + + DataServer dataServer(argc, argv); + + return dataServer.exec(); +} \ No newline at end of file diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 28edf6b6a1..3e60d1509c 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -22,6 +23,10 @@ const int RESTART_HOLD_TIME_MSECS = 5 * 1000; +const char* VOXEL_SERVER_CONFIG = "voxelServerConfig"; +const char* PARTICLE_SERVER_CONFIG = "particleServerConfig"; +const char* METAVOXEL_SERVER_CONFIG = "metavoxelServerConfig"; + void signalhandler(int sig){ if (sig == SIGINT) { qApp->quit(); @@ -47,17 +52,22 @@ DomainServer::DomainServer(int argc, char* argv[]) : const char* customPortString = getCmdOption(argc, (const char**) argv, CUSTOM_PORT_OPTION); unsigned short domainServerPort = customPortString ? atoi(customPortString) : DEFAULT_DOMAIN_SERVER_PORT; + const char CONFIG_FILE_OPTION[] = "-c"; + const char* configFilePath = getCmdOption(argc, (const char**) argv, CONFIG_FILE_OPTION); + + if (!readConfigFile(configFilePath)) { + QByteArray voxelConfigOption = QString("--%1").arg(VOXEL_SERVER_CONFIG).toLocal8Bit(); + _voxelServerConfig = getCmdOption(argc, (const char**) argv, voxelConfigOption.constData()); + + QByteArray particleConfigOption = QString("--%1").arg(PARTICLE_SERVER_CONFIG).toLocal8Bit(); + _particleServerConfig = getCmdOption(argc, (const char**) argv, particleConfigOption.constData()); + + QByteArray metavoxelConfigOption = QString("--%1").arg(METAVOXEL_SERVER_CONFIG).toLocal8Bit(); + _metavoxelServerConfig = getCmdOption(argc, (const char**) argv, metavoxelConfigOption.constData()); + } + NodeList* nodeList = NodeList::createInstance(NODE_TYPE_DOMAIN, domainServerPort); - const char VOXEL_CONFIG_OPTION[] = "--voxelServerConfig"; - _voxelServerConfig = getCmdOption(argc, (const char**) argv, VOXEL_CONFIG_OPTION); - - const char PARTICLE_CONFIG_OPTION[] = "--particleServerConfig"; - _particleServerConfig = getCmdOption(argc, (const char**) argv, PARTICLE_CONFIG_OPTION); - - const char METAVOXEL_CONFIG_OPTION[] = "--metavoxelServerConfig"; - _metavoxelServerConfig = getCmdOption(argc, (const char**)argv, METAVOXEL_CONFIG_OPTION); - connect(nodeList, SIGNAL(nodeKilled(SharedNodePointer)), this, SLOT(nodeKilled(SharedNodePointer))); if (!_staticAssignmentFile.exists() || _voxelServerConfig) { @@ -275,6 +285,70 @@ QJsonObject jsonObjectForNode(Node* node) { return nodeJson; } +// Attempts to read configuration from specified path +// returns true on success, false otherwise +bool DomainServer::readConfigFile(const char* path) { + if (!path) { + // config file not specified + return false; + } + + if (!QFile::exists(path)) { + qWarning("Specified configuration file does not exist!\n"); + return false; + } + + QFile configFile(path); + if (!configFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning("Can't open specified configuration file!\n"); + return false; + } else { + qDebug("Reading configuration from %s\n", path); + } + QTextStream configStream(&configFile); + QByteArray configStringByteArray = configStream.readAll().toUtf8(); + QJsonObject configDocObject = QJsonDocument::fromJson(configStringByteArray).object(); + configFile.close(); + + QString voxelServerConfig = readServerAssignmentConfig(configDocObject, VOXEL_SERVER_CONFIG); + _voxelServerConfig = new char[strlen(voxelServerConfig.toLocal8Bit().constData()) +1]; + _voxelServerConfig = strcpy((char *) _voxelServerConfig, voxelServerConfig.toLocal8Bit().constData() + '\0'); + + QString particleServerConfig = readServerAssignmentConfig(configDocObject, PARTICLE_SERVER_CONFIG); + _particleServerConfig = new char[strlen(particleServerConfig.toLocal8Bit().constData()) +1]; + _particleServerConfig = strcpy((char *) _particleServerConfig, particleServerConfig.toLocal8Bit().constData() + '\0'); + + QString metavoxelServerConfig = readServerAssignmentConfig(configDocObject, METAVOXEL_SERVER_CONFIG); + _metavoxelServerConfig = new char[strlen(metavoxelServerConfig.toLocal8Bit().constData()) +1]; + _metavoxelServerConfig = strcpy((char *) _metavoxelServerConfig, metavoxelServerConfig.toLocal8Bit().constData() + '\0'); + + return true; +} + +// find assignment configurations on the specified node name and json object +// returns a string in the form of its equivalent cmd line params +QString DomainServer::readServerAssignmentConfig(QJsonObject jsonObject, const char* nodeName) { + QJsonArray nodeArray = jsonObject[nodeName].toArray(); + + QStringList serverConfig; + foreach (const QJsonValue & childValue, nodeArray) { + QString cmdParams; + QJsonObject childObject = childValue.toObject(); + QStringList keys = childObject.keys(); + for (int i = 0; i < keys.size(); i++) { + QString key = keys[i]; + QString value = childObject[key].toString(); + // both cmd line params and json keys are the same + cmdParams += QString("--%1 %2 ").arg(key, value); + } + serverConfig << cmdParams; + } + + // according to split() calls from DomainServer::prepopulateStaticAssignmentFile + // we shold simply join them with semicolons + return serverConfig.join(';'); +} + bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QString& path) { const QString JSON_MIME_TYPE = "application/json"; diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index c9c4f0e2d7..fed5aaaa43 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -35,6 +35,9 @@ public slots: void nodeKilled(SharedNodePointer node); private: + bool readConfigFile(const char* path); + QString readServerAssignmentConfig(QJsonObject jsonObj, const char* nodeName); + void prepopulateStaticAssignmentFile(); Assignment* matchingStaticAssignmentForCheckIn(const QUuid& checkInUUID, NODE_TYPE nodeType); Assignment* deployableAssignmentForRequest(Assignment& requestAssignment); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index a0fc3f332f..d05ea7a7c0 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -15,7 +15,7 @@ set(SIXENSE_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/Sixense) if (DEFINED ENV{JOB_ID}) set(BUILD_SEQ $ENV{JOB_ID}) else () - set(BUILD_SEQ "0") + set(BUILD_SEQ "dev") endif () if (APPLE) @@ -80,6 +80,8 @@ find_package(Qt5OpenGL REQUIRED) find_package(Qt5Svg REQUIRED) find_package(Qt5WebKit REQUIRED) find_package(Qt5WebKitWidgets REQUIRED) +find_package(Qt5Xml REQUIRED) +find_package(Qt5UiTools REQUIRED) if (APPLE) set(MACOSX_BUNDLE_BUNDLE_NAME Interface) @@ -146,7 +148,7 @@ if (LIBOVR_FOUND AND NOT DISABLE_LIBOVR) target_link_libraries(${TARGET_NAME} ${LIBOVR_LIBRARIES}) endif (LIBOVR_FOUND AND NOT DISABLE_LIBOVR) -qt5_use_modules(${TARGET_NAME} Core Gui Multimedia Network OpenGL Script Svg WebKit WebKitWidgets) +qt5_use_modules(${TARGET_NAME} Core Gui Multimedia Network OpenGL Script Svg WebKit WebKitWidgets Xml UiTools) # include headers for interface and InterfaceConfig. include_directories( diff --git a/interface/InterfaceVersion.h.in b/interface/InterfaceVersion.h.in index 26b2260965..60e51b4ab6 100644 --- a/interface/InterfaceVersion.h.in +++ b/interface/InterfaceVersion.h.in @@ -6,4 +6,4 @@ // Copyright (c) 2013 High Fidelity, Inc.. All rights reserved. // -const int BUILD_VERSION = @BUILD_SEQ@; +const QString BUILD_VERSION = "@BUILD_SEQ@"; diff --git a/interface/resources/info/ApplicationInfo.ini b/interface/resources/info/ApplicationInfo.ini index a1d1603172..35a6c71eb8 100644 --- a/interface/resources/info/ApplicationInfo.ini +++ b/interface/resources/info/ApplicationInfo.ini @@ -1,5 +1,4 @@ [INFO] name=Interface -version=0.0.1 organizationName=High Fidelity -organizationDomain=highfidelity.io \ No newline at end of file +organizationDomain=highfidelity.io diff --git a/interface/resources/ui/updateDialog.ui b/interface/resources/ui/updateDialog.ui new file mode 100644 index 0000000000..b0f1e63cb9 --- /dev/null +++ b/interface/resources/ui/updateDialog.ui @@ -0,0 +1,182 @@ + + + Dialog + + + Qt::NonModal + + + + 0 + 0 + 750 + 300 + + + + PointingHandCursor + + + Update Required + + + background-color: rgb(255, 255, 255); + + + + + 0 + 0 + 751 + 71 + + + + Qt::LeftToRight + + + false + + + background-color: rgb(236, 236, 236); + + + QFrame::StyledPanel + + + QFrame::Plain + + + 0 + + + + + 240 + 10 + 271 + 41 + + + + + Arial + 36 + PreferAntialias + false + + + + color: rgb(51, 51, 51); + + + Update Required + + + + + + + 100 + 110 + 561 + 61 + + + + + Arial + 18 + 50 + false + + + + + + + true + + + + + + 360 + 240 + 364 + 40 + + + + + + + PointingHandCursor + + + background-color: #333333; + border-width: 0; + border-radius: 9px; + border-radius: 9px; + font-family: Arial; + font-size: 18px; + font-weight: 100; + color: #b7b7b7; + width: 120px; + height: 40px; + + + Download + + + + + + + PointingHandCursor + + + background-color: #333333; + border-width: 0; + border-radius: 9px; + border-radius: 9px; + font-family: Arial; + font-size: 18px; + font-weight: 100; + color: #b7b7b7; + width: 120px; + height: 40px; + + + Skip Version + + + + + + + PointingHandCursor + + + background-color: #333333; + border-width: 0; + border-radius: 9px; + border-radius: 9px; + font-family: Arial; + font-size: 18px; + font-weight: 100; + color: #b7b7b7; + width: 120px; + height: 40px; + + + Close + + + + + + + + + diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d0e74a9f4c..f4209ba892 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -33,8 +33,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -43,6 +45,8 @@ #include #include #include +#include +#include #include #include @@ -91,6 +95,9 @@ const float MIRROR_FULLSCREEN_DISTANCE = 0.35f; const float MIRROR_REARVIEW_DISTANCE = 0.65f; const float MIRROR_REARVIEW_BODY_DISTANCE = 2.3f; +const QString CHECK_VERSION_URL = "http://highfidelity.io/latestVersion.xml"; +const QString SKIP_FILENAME = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/hifi.skipversion"; + void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString &message) { QString messageWithNewLine = message + "\n"; fprintf(stdout, "%s", messageWithNewLine.toLocal8Bit().constData()); @@ -160,8 +167,6 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : // call Menu getInstance static method to set up the menu _window->setMenuBar(Menu::getInstance()); - qDebug("[VERSION] Build sequence: %i", BUILD_VERSION); - unsigned int listenPort = 0; // bind to an ephemeral port by default const char** constArgv = const_cast(argv); const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); @@ -195,9 +200,11 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : applicationInfo.beginGroup("INFO"); setApplicationName(applicationInfo.value("name").toString()); - setApplicationVersion(applicationInfo.value("version").toString()); + setApplicationVersion(BUILD_VERSION); setOrganizationName(applicationInfo.value("organizationName").toString()); setOrganizationDomain(applicationInfo.value("organizationDomain").toString()); + + qDebug() << "[VERSION] Build sequence: " << qPrintable(applicationVersion()); _settings = new QSettings(this); @@ -232,6 +239,7 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : _window->setCentralWidget(_glWidget); restoreSizeAndPosition(); + loadScripts(); _window->setVisible(true); _glWidget->setFocusPolicy(Qt::StrongFocus); _glWidget->setFocus(); @@ -255,7 +263,8 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : // Set the sixense filtering _sixenseManager.setFilter(Menu::getInstance()->isOptionChecked(MenuOption::FilterSixense)); - + + checkVersion(); } Application::~Application() { @@ -270,7 +279,7 @@ Application::~Application() { _audio.thread()->wait(); storeSizeAndPosition(); - + saveScripts(); _sharedVoxelSystem.changeTree(new VoxelTree); VoxelTreeElement::removeDeleteHook(&_voxels); // we don't need to do this processing on shutdown @@ -1313,6 +1322,9 @@ void Application::timer() { // ask the node list to check in with the domain server NodeList::getInstance()->sendDomainServerCheckIn(); + + // send unmatched DataServerClient packets + DataServerClient::resendUnmatchedPackets(); // give the MyAvatar object position, orientation to the Profile so it can propagate to the data-server _profile.updatePosition(_myAvatar.getPosition()); @@ -1382,6 +1394,7 @@ void Application::idle() { } } } + void Application::terminate() { // Close serial port // close(serial_fd); @@ -1404,55 +1417,6 @@ void Application::terminate() { } } -static Avatar* processAvatarMessageHeader(unsigned char*& packetData, size_t& dataBytes) { - // record the packet for stats-tracking - Application::getInstance()->getBandwidthMeter()->inputStream(BandwidthMeter::AVATARS).updateValue(dataBytes); - SharedNodePointer avatarMixerNode = NodeList::getInstance()->soloNodeOfType(NODE_TYPE_AVATAR_MIXER); - if (avatarMixerNode) { - avatarMixerNode->recordBytesReceived(dataBytes); - } - - // skip the header - int numBytesPacketHeader = numBytesForPacketHeader(packetData); - packetData += numBytesPacketHeader; - dataBytes -= numBytesPacketHeader; - - // read the node id - QUuid nodeUUID = QUuid::fromRfc4122(QByteArray((char*) packetData, NUM_BYTES_RFC4122_UUID)); - - packetData += NUM_BYTES_RFC4122_UUID; - dataBytes -= NUM_BYTES_RFC4122_UUID; - - // make sure the node exists - SharedNodePointer node = NodeList::getInstance()->nodeWithUUID(nodeUUID); - if (!node || !node->getLinkedData()) { - return NULL; - } - Avatar* avatar = static_cast(node->getLinkedData()); - return avatar->isInitialized() ? avatar : NULL; -} - -void Application::processAvatarURLsMessage(unsigned char* packetData, size_t dataBytes) { - Avatar* avatar = processAvatarMessageHeader(packetData, dataBytes); - if (!avatar) { - return; - } - // PER Note: message is no longer processed but used to trigger - // Dataserver lookup - redesign this to instantly ask the - // dataserver on first receipt of other avatar UUID, and also - // don't ask over and over again. Instead use this message to - // Tell the other avatars that your dataserver data has - // changed. - - //QDataStream in(QByteArray((char*)packetData, dataBytes)); - //QUrl voxelURL; - //in >> voxelURL; - - // use this timing to as the data-server for an updated mesh for this avatar (if we have UUID) - DataServerClient::getValuesForKeysAndUUID(QStringList() << DataServerKey::FaceMeshURL << DataServerKey::SkeletonURL, - avatar->getUUID()); -} - void Application::checkBandwidthMeterClick() { // ... to be called upon button release @@ -1802,8 +1766,8 @@ void Application::init() { if (!_profile.getUsername().isEmpty()) { // we have a username for this avatar, ask the data-server for the mesh URL for this avatar - DataServerClient::getClientValueForKey(DataServerKey::FaceMeshURL); - DataServerClient::getClientValueForKey(DataServerKey::SkeletonURL); + DataServerClient::getValueForKeyAndUserString(DataServerKey::FaceMeshURL, _profile.getUserString(), &_profile); + DataServerClient::getValueForKeyAndUserString(DataServerKey::SkeletonURL, _profile.getUserString(), &_profile); } // Set up VoxelSystem after loading preferences so we can get the desired max voxel count @@ -2492,11 +2456,6 @@ void Application::updateAvatar(float deltaTime) { controlledBroadcastToNodes(broadcastString, endOfBroadcastStringWrite - broadcastString, nodeTypesOfInterest, sizeof(nodeTypesOfInterest)); - const float AVATAR_URLS_SEND_INTERVAL = 1.0f; - if (shouldDo(AVATAR_URLS_SEND_INTERVAL, deltaTime)) { - QUrl empty; - Avatar::sendAvatarURLsMessage(empty); - } // Update _viewFrustum with latest camera and view frustum data... // NOTE: we get this from the view frustum, to make it simpler, since the // loadViewFrumstum() method will get the correct details from the camera @@ -3903,13 +3862,16 @@ void Application::attachNewHeadToNode(Node* newNode) { void Application::updateWindowTitle(){ QString title = ""; - QString buildVersion = " (build " + QString::number(BUILD_VERSION) + ")"; + + QString buildVersion = " (build " + applicationVersion() + ")"; + QString username = _profile.getUsername(); if(!username.isEmpty()){ - title += _profile.getUsername(); + title += username; title += " @ "; } - title += _profile.getLastDomain(); + + title += NodeList::getInstance()->getDomainHostname(); title += buildVersion; qDebug("Application title set to: %s", title.toStdString().c_str()); @@ -4161,9 +4123,6 @@ void Application::processDatagrams() { bytesReceived); getInstance()->_bandwidthMeter.inputStream(BandwidthMeter::AVATARS).updateValue(bytesReceived); break; - case PACKET_TYPE_AVATAR_URLS: - processAvatarURLsMessage(_incomingPacket, bytesReceived); - break; case PACKET_TYPE_DATA_SERVER_GET: case PACKET_TYPE_DATA_SERVER_PUT: case PACKET_TYPE_DATA_SERVER_SEND: @@ -4182,13 +4141,38 @@ void Application::packetSentNotification(ssize_t length) { _bandwidthMeter.outputStream(BandwidthMeter::VOXELS).updateValue(length); } -void Application::loadScript() { - // shut down and stop any existing script - QString desktopLocation = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); - QString suggestedName = desktopLocation.append("/script.js"); +void Application::loadScripts(){ + // loads all saved scripts + QSettings* settings = new QSettings(this); + int size = settings->beginReadArray("Settings"); + for(int i=0; isetArrayIndex(i); + QString string = settings->value("script").toString(); + loadScript(string); + } + settings->endArray(); - QString fileNameString = QFileDialog::getOpenFileName(_glWidget, tr("Open Script"), suggestedName, - tr("JavaScript Files (*.js)")); +} + +void Application::saveScripts(){ + // saves all current running scripts + QSettings* settings = new QSettings(this); + settings->beginWriteArray("Settings"); + for(int i=0; i<_activeScripts.size(); ++i){ + settings->setArrayIndex(i); + settings->setValue("script", _activeScripts.at(i)); + } + settings->endArray(); + +} + +void Application::removeScriptName(const QString& fileNameString) +{ + _activeScripts.removeOne(fileNameString); +} + +void Application::loadScript(const QString& fileNameString){ + _activeScripts.append(fileNameString); QByteArray fileNameAscii = fileNameString.toLocal8Bit(); const char* fileName = fileNameAscii.data(); @@ -4215,9 +4199,7 @@ void Application::loadScript() { // start the script on a new thread... bool wantMenuItems = true; // tells the ScriptEngine object to add menu items for itself - - ScriptEngine* scriptEngine = new ScriptEngine(script, wantMenuItems, fileName, Menu::getInstance(), - &_controllerScriptingInterface); + ScriptEngine* scriptEngine = new ScriptEngine(script, wantMenuItems, fileName, Menu::getInstance(), &_controllerScriptingInterface); scriptEngine->setupMenuItems(); // setup the packet senders and jurisdiction listeners of the script engine's scripting interfaces so @@ -4232,8 +4214,9 @@ void Application::loadScript() { connect(workerThread, SIGNAL(started()), scriptEngine, SLOT(run())); // when the thread is terminated, add both scriptEngine and thread to the deleteLater queue - connect(scriptEngine, SIGNAL(finished()), scriptEngine, SLOT(deleteLater())); + connect(scriptEngine, SIGNAL(finished(const QString&)), scriptEngine, SLOT(deleteLater())); connect(workerThread, SIGNAL(finished()), workerThread, SLOT(deleteLater())); + connect(scriptEngine, SIGNAL(finished(const QString&)), this, SLOT(removeScriptName(const QString&))); // when the application is about to quit, stop our script engine so it unwinds properly connect(this, SIGNAL(aboutToQuit()), scriptEngine, SLOT(stop())); @@ -4247,6 +4230,17 @@ void Application::loadScript() { _window->activateWindow(); } +void Application::loadDialog() { + // shut down and stop any existing script + QString desktopLocation = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + QString suggestedName = desktopLocation.append("/script.js"); + + QString fileNameString = QFileDialog::getOpenFileName(_glWidget, tr("Open Script"), suggestedName, + tr("JavaScript Files (*.js)")); + + loadScript(fileNameString); +} + void Application::toggleLogDialog() { if (! _logDialog) { _logDialog = new LogDialog(_glWidget, getLogger()); @@ -4256,7 +4250,6 @@ void Application::toggleLogDialog() { } } - void Application::initAvatarAndViewFrustum() { updateAvatar(0.f); } @@ -4303,3 +4296,72 @@ void Application::updateLocalOctreeCache(bool firstTime) { } } } + +void Application::checkVersion() { + QNetworkRequest latestVersionRequest((QUrl(CHECK_VERSION_URL))); + latestVersionRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); + connect(Application::getInstance()->getNetworkAccessManager()->get(latestVersionRequest), SIGNAL(finished()), SLOT(parseVersionXml())); +} + +void Application::parseVersionXml() { + + #ifdef Q_OS_WIN32 + QString operatingSystem("win"); + #endif + + #ifdef Q_OS_MAC + QString operatingSystem("mac"); + #endif + + #ifdef Q_OS_LINUX + QString operatingSystem("ubuntu"); + #endif + + QString releaseDate; + QString releaseNotes; + QString latestVersion; + QUrl downloadUrl; + QObject* sender = QObject::sender(); + + QXmlStreamReader xml(qobject_cast(sender)); + while (!xml.atEnd() && !xml.hasError()) { + QXmlStreamReader::TokenType token = xml.readNext(); + + if (token == QXmlStreamReader::StartElement) { + if (xml.name() == "ReleaseDate") { + xml.readNext(); + releaseDate = xml.text().toString(); + } + if (xml.name() == "ReleaseNotes") { + xml.readNext(); + releaseNotes = xml.text().toString(); + } + if (xml.name() == "Version") { + xml.readNext(); + latestVersion = xml.text().toString(); + } + if (xml.name() == operatingSystem) { + xml.readNext(); + downloadUrl = QUrl(xml.text().toString()); + } + } + } + if (!shouldSkipVersion(latestVersion) && applicationVersion() != latestVersion) { + new UpdateDialog(_glWidget, releaseNotes, latestVersion, downloadUrl); + } + sender->deleteLater(); +} + +bool Application::shouldSkipVersion(QString latestVersion) { + QFile skipFile(SKIP_FILENAME); + skipFile.open(QIODevice::ReadWrite); + QString skipVersion(skipFile.readAll()); + return (skipVersion == latestVersion || applicationVersion() == "dev"); +} + +void Application::skipVersion(QString latestVersion) { + QFile skipFile(SKIP_FILENAME); + skipFile.open(QIODevice::WriteOnly | QIODevice::Truncate); + skipFile.seek(0); + skipFile.write(latestVersion.toStdString().c_str()); +} diff --git a/interface/src/Application.h b/interface/src/Application.h index 3c03fb0d66..6be01db39f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -64,6 +65,7 @@ #include "ui/RearMirrorTools.h" #include "ui/LodToolsDialog.h" #include "ui/LogDialog.h" +#include "ui/UpdateDialog.h" #include "FileLogger.h" #include "ParticleTreeRenderer.h" #include "ControllerScriptingInterface.h" @@ -102,7 +104,10 @@ public: ~Application(); void restoreSizeAndPosition(); + void loadScript(const QString& fileNameString); + void loadScripts(); void storeSizeAndPosition(); + void saveScripts(); void initializeGL(); void paintGL(); void resizeGL(int width, int height); @@ -196,6 +201,8 @@ public: /// set a voxel which is to be rendered with a highlight void setHighlightVoxel(const VoxelDetail& highlightVoxel) { _highlightVoxel = highlightVoxel; } void setIsHighlightVoxel(bool isHighlightVoxel) { _isHighlightVoxel = isHighlightVoxel; } + + void skipVersion(QString latestVersion); public slots: void domainChanged(const QString& domainHostname); @@ -215,7 +222,7 @@ public slots: void doKillLocalVoxels(); void decreaseVoxelSize(); void increaseVoxelSize(); - void loadScript(); + void loadDialog(); void toggleLogDialog(); void initAvatarAndViewFrustum(); @@ -243,6 +250,10 @@ private slots: void restoreMirrorView(); void shrinkMirrorView(); void resetSensors(); + + void parseVersionXml(); + + void removeScriptName(const QString& fileNameString); private: void resetCamerasOnResizeGL(Camera& camera, int width, int height); @@ -250,7 +261,6 @@ private: void updateProjectionMatrix(Camera& camera, bool updateViewFrustum = true); static bool sendVoxelsOperation(OctreeElement* node, void* extraData); - static void processAvatarURLsMessage(unsigned char* packetData, size_t dataBytes); static void sendPingPackets(); void initDisplay(); @@ -367,6 +377,7 @@ private: Faceshift _faceshift; SixenseManager _sixenseManager; + QStringList _activeScripts; Camera _myCamera; // My view onto the world Camera _viewFrustumOffsetCamera; // The camera we use to sometimes show the view frustum from an offset mode @@ -489,6 +500,10 @@ private: QString getLocalVoxelCacheFileName(); void updateLocalOctreeCache(bool firstTime = false); + + void checkVersion(); + void displayUpdateDialog(); + bool shouldSkipVersion(QString latestVersion); }; #endif /* defined(__interface__Application__) */ diff --git a/interface/src/ControllerScriptingInterface.h b/interface/src/ControllerScriptingInterface.h index d0e032d52f..69daefa3fb 100644 --- a/interface/src/ControllerScriptingInterface.h +++ b/interface/src/ControllerScriptingInterface.h @@ -12,6 +12,7 @@ #include #include +class PalmData; /// handles scripting of input controller commands from JS class ControllerScriptingInterface : public AbstractControllerScriptingInterface { diff --git a/interface/src/DataServerClient.cpp b/interface/src/DataServerClient.cpp deleted file mode 100644 index 45bdd6f6a9..0000000000 --- a/interface/src/DataServerClient.cpp +++ /dev/null @@ -1,260 +0,0 @@ -// -// DataServerClient.cpp -// hifi -// -// Created by Stephen Birarda on 10/7/13. -// Copyright (c) 2013 HighFidelity, Inc. All rights reserved. -// - -#include -#include - -#include -#include -#include - -#include "Application.h" -#include "avatar/Profile.h" - -#include "DataServerClient.h" - -std::map DataServerClient::_unmatchedPackets; - -const char MULTI_KEY_VALUE_SEPARATOR = '|'; - -const char DATA_SERVER_HOSTNAME[] = "data.highfidelity.io"; -const unsigned short DATA_SERVER_PORT = 3282; - - -const HifiSockAddr& DataServerClient::dataServerSockAddr() { - static HifiSockAddr dsSockAddr = HifiSockAddr(DATA_SERVER_HOSTNAME, DATA_SERVER_PORT); - return dsSockAddr; -} - -void DataServerClient::putValueForKey(const QString& key, const char* value) { - QString clientString = Application::getInstance()->getProfile()->getUserString(); - if (!clientString.isEmpty()) { - - unsigned char* putPacket = new unsigned char[MAX_PACKET_SIZE]; - - // setup the header for this packet - int numPacketBytes = populateTypeAndVersion(putPacket, PACKET_TYPE_DATA_SERVER_PUT); - - // pack the client UUID, null terminated - memcpy(putPacket + numPacketBytes, clientString.toLocal8Bit().constData(), clientString.toLocal8Bit().size()); - numPacketBytes += clientString.toLocal8Bit().size(); - putPacket[numPacketBytes++] = '\0'; - - // pack a 1 to designate that we are putting a single value - putPacket[numPacketBytes++] = 1; - - // pack the key, null terminated - strcpy((char*) putPacket + numPacketBytes, key.toLocal8Bit().constData()); - numPacketBytes += key.size(); - putPacket[numPacketBytes++] = '\0'; - - // pack the value, null terminated - strcpy((char*) putPacket + numPacketBytes, value); - numPacketBytes += strlen(value); - putPacket[numPacketBytes++] = '\0'; - - // add the putPacket to our vector of unconfirmed packets, will be deleted once put is confirmed - // _unmatchedPackets.insert(std::pair(putPacket, numPacketBytes)); - - // send this put request to the data server - NodeList::getInstance()->getNodeSocket().writeDatagram((char*) putPacket, numPacketBytes, - dataServerSockAddr().getAddress(), - dataServerSockAddr().getPort()); - } -} - -void DataServerClient::getValueForKeyAndUUID(const QString& key, const QUuid &uuid) { - getValuesForKeysAndUUID(QStringList(key), uuid); -} - -void DataServerClient::getValuesForKeysAndUUID(const QStringList& keys, const QUuid& uuid) { - if (!uuid.isNull()) { - getValuesForKeysAndUserString(keys, uuidStringWithoutCurlyBraces(uuid)); - } -} - -void DataServerClient::getValuesForKeysAndUserString(const QStringList& keys, const QString& userString) { - if (!userString.isEmpty() && keys.size() <= UCHAR_MAX) { - unsigned char* getPacket = new unsigned char[MAX_PACKET_SIZE]; - - // setup the header for this packet - int numPacketBytes = populateTypeAndVersion(getPacket, PACKET_TYPE_DATA_SERVER_GET); - - // pack the user string (could be username or UUID string), null-terminate - memcpy(getPacket + numPacketBytes, userString.toLocal8Bit().constData(), userString.toLocal8Bit().size()); - numPacketBytes += userString.toLocal8Bit().size(); - getPacket[numPacketBytes++] = '\0'; - - // pack one byte to designate the number of keys - getPacket[numPacketBytes++] = keys.size(); - - QString keyString = keys.join(MULTI_KEY_VALUE_SEPARATOR); - - // pack the key string, null terminated - strcpy((char*) getPacket + numPacketBytes, keyString.toLocal8Bit().constData()); - numPacketBytes += keyString.size() + sizeof('\0'); - - // add the getPacket to our vector of uncofirmed packets, will be deleted once we get a response from the nameserver - // _unmatchedPackets.insert(std::pair(getPacket, numPacketBytes)); - - // send the get to the data server - NodeList::getInstance()->getNodeSocket().writeDatagram((char*) getPacket, numPacketBytes, - dataServerSockAddr().getAddress(), - dataServerSockAddr().getPort()); - } -} - -void DataServerClient::getClientValueForKey(const QString& key) { - getValuesForKeysAndUserString(QStringList(key), Application::getInstance()->getProfile()->getUserString()); -} - -void DataServerClient::processConfirmFromDataServer(unsigned char* packetData, int numPacketBytes) { - removeMatchedPacketFromMap(packetData, numPacketBytes); -} - -void DataServerClient::processSendFromDataServer(unsigned char* packetData, int numPacketBytes) { - // pull the user string from the packet so we know who to associate this with - int numHeaderBytes = numBytesForPacketHeader(packetData); - - char* userStringPosition = (char*) packetData + numHeaderBytes; - - QString userString(QByteArray(userStringPosition, strlen(userStringPosition))); - - QUuid userUUID(userString); - - char* keysPosition = (char*) packetData + numHeaderBytes + strlen(userStringPosition) - + sizeof('\0') + sizeof(unsigned char); - char* valuesPosition = keysPosition + strlen(keysPosition) + sizeof('\0'); - - QStringList keyList = QString(keysPosition).split(MULTI_KEY_VALUE_SEPARATOR); - QStringList valueList = QString(valuesPosition).split(MULTI_KEY_VALUE_SEPARATOR); - - // user string was UUID, find matching avatar and associate data - for (int i = 0; i < keyList.size(); i++) { - if (valueList[i] != " ") { - if (keyList[i] == DataServerKey::FaceMeshURL) { - - if (userUUID.isNull() || userUUID == Application::getInstance()->getProfile()->getUUID()) { - qDebug("Changing user's face model URL to %s", valueList[i].toLocal8Bit().constData()); - Application::getInstance()->getProfile()->setFaceModelURL(QUrl(valueList[i])); - } else { - // mesh URL for a UUID, find avatar in our list - - foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { - if (node->getLinkedData() != NULL && node->getType() == NODE_TYPE_AGENT) { - Avatar* avatar = (Avatar *) node->getLinkedData(); - - if (avatar->getUUID() == userUUID) { - QMetaObject::invokeMethod(&avatar->getHead().getFaceModel(), - "setURL", Q_ARG(QUrl, QUrl(valueList[i]))); - } - } - } - } - } else if (keyList[i] == DataServerKey::SkeletonURL) { - - if (userUUID.isNull() || userUUID == Application::getInstance()->getProfile()->getUUID()) { - qDebug("Changing user's skeleton URL to %s", valueList[i].toLocal8Bit().constData()); - Application::getInstance()->getProfile()->setSkeletonModelURL(QUrl(valueList[i])); - } else { - // skeleton URL for a UUID, find avatar in our list - foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { - if (node->getLinkedData() != NULL && node->getType() == NODE_TYPE_AGENT) { - Avatar* avatar = (Avatar *) node->getLinkedData(); - - if (avatar->getUUID() == userUUID) { - QMetaObject::invokeMethod(&avatar->getSkeletonModel(), "setURL", - Q_ARG(QUrl, QUrl(valueList[i]))); - } - } - } - } - } else if (keyList[i] == DataServerKey::Domain && keyList[i + 1] == DataServerKey::Position && - keyList[i + 2] == DataServerKey::Orientation && valueList[i] != " " && - valueList[i + 1] != " " && valueList[i + 2] != " ") { - - QStringList coordinateItems = valueList[i + 1].split(','); - QStringList orientationItems = valueList[i + 2].split(','); - - if (coordinateItems.size() == 3 && orientationItems.size() == 3) { - - // send a node kill request, indicating to other clients that they should play the "disappeared" effect - NodeList::getInstance()->sendKillNode(&NODE_TYPE_AVATAR_MIXER, 1); - - qDebug() << "Changing domain to" << valueList[i].toLocal8Bit().constData() << - ", position to" << valueList[i + 1].toLocal8Bit().constData() << - ", and orientation to" << valueList[i + 2].toLocal8Bit().constData() << - "to go to" << userString; - - NodeList::getInstance()->setDomainHostname(valueList[i]); - // orient the user to face the target - glm::quat newOrientation = glm::quat(glm::radians(glm::vec3(orientationItems[0].toFloat(), - orientationItems[1].toFloat(), orientationItems[2].toFloat()))) * - glm::angleAxis(180.0f, 0.0f, 1.0f, 0.0f); - Application::getInstance()->getAvatar()->setOrientation(newOrientation); - - // move the user a couple units away - const float DISTANCE_TO_USER = 2.0f; - glm::vec3 newPosition = glm::vec3(coordinateItems[0].toFloat(), coordinateItems[1].toFloat(), - coordinateItems[2].toFloat()) - newOrientation * IDENTITY_FRONT * DISTANCE_TO_USER; - Application::getInstance()->getAvatar()->setPosition(newPosition); - } - - } else if (keyList[i] == DataServerKey::UUID) { - // this is the user's UUID - set it on the profile - Application::getInstance()->getProfile()->setUUID(valueList[i]); - } - } - } - - // remove the matched packet from our map so it isn't re-sent to the data-server - // removeMatchedPacketFromMap(packetData, numPacketBytes); -} - -void DataServerClient::processMessageFromDataServer(unsigned char* packetData, int numPacketBytes) { - switch (packetData[0]) { - case PACKET_TYPE_DATA_SERVER_SEND: - processSendFromDataServer(packetData, numPacketBytes); - break; - case PACKET_TYPE_DATA_SERVER_CONFIRM: - processConfirmFromDataServer(packetData, numPacketBytes); - break; - default: - break; - } -} - -void DataServerClient::removeMatchedPacketFromMap(unsigned char* packetData, int numPacketBytes) { - for (std::map::iterator mapIterator = _unmatchedPackets.begin(); - mapIterator != _unmatchedPackets.end(); - ++mapIterator) { - if (memcmp(mapIterator->first + sizeof(PACKET_TYPE), - packetData + sizeof(PACKET_TYPE), - numPacketBytes - sizeof(PACKET_TYPE)) == 0) { - - // this is a match - remove the confirmed packet from the vector and delete associated member - // so it isn't sent back out - delete[] mapIterator->first; - _unmatchedPackets.erase(mapIterator); - - // we've matched the packet - bail out - break; - } - } -} - -void DataServerClient::resendUnmatchedPackets() { - for (std::map::iterator mapIterator = _unmatchedPackets.begin(); - mapIterator != _unmatchedPackets.end(); - ++mapIterator) { - // send the unmatched packet to the data server - NodeList::getInstance()->getNodeSocket().writeDatagram((char*) mapIterator->first, mapIterator->second, - dataServerSockAddr().getAddress(), - dataServerSockAddr().getPort()); - } -} diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 7299615ecc..1723e10f61 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -91,7 +91,7 @@ Menu::Menu() : SLOT(login()))); addDisabledActionAndSeparator(fileMenu, "Scripts"); - addActionToQMenuAndActionHash(fileMenu, MenuOption::LoadScript, Qt::CTRL | Qt::Key_O, appInstance, SLOT(loadScript())); + addActionToQMenuAndActionHash(fileMenu, MenuOption::LoadScript, Qt::CTRL | Qt::Key_O, appInstance, SLOT(loadDialog())); _activeScriptsMenu = fileMenu->addMenu("Running Scripts"); addDisabledActionAndSeparator(fileMenu, "Voxels"); @@ -830,8 +830,9 @@ void Menu::editPreferences() { applicationInstance->getProfile()->setFaceModelURL(faceModelURL); // send the new face mesh URL to the data-server (if we have a client UUID) - DataServerClient::putValueForKey(DataServerKey::FaceMeshURL, - faceModelURL.toString().toLocal8Bit().constData()); + DataServerClient::putValueForKeyAndUserString(DataServerKey::FaceMeshURL, + faceModelURL.toString().toLocal8Bit().constData(), + applicationInstance->getProfile()->getUserString()); } QUrl skeletonModelURL(skeletonURLEdit->text()); @@ -841,8 +842,9 @@ void Menu::editPreferences() { applicationInstance->getProfile()->setSkeletonModelURL(skeletonModelURL); // send the new skeleton model URL to the data-server (if we have a client UUID) - DataServerClient::putValueForKey(DataServerKey::SkeletonURL, - skeletonModelURL.toString().toLocal8Bit().constData()); + DataServerClient::putValueForKeyAndUserString(DataServerKey::SkeletonURL, + skeletonModelURL.toString().toLocal8Bit().constData(), + applicationInstance->getProfile()->getUserString()); } applicationInstance->getAvatar()->getHead().setPupilDilation(pupilDilation->value() / (float)pupilDilation->maximum()); @@ -963,7 +965,7 @@ void Menu::goToUser() { // there's a username entered by the user, make a request to the data-server DataServerClient::getValuesForKeysAndUserString( QStringList() << DataServerKey::Domain << DataServerKey::Position << DataServerKey::Orientation, - userDialog.textValue()); + userDialog.textValue(), Application::getInstance()->getProfile()); } sendFakeEnterEvent(); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 6db0a15300..30de5b1e2a 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -58,21 +58,6 @@ const int NUM_BODY_CONE_SIDES = 9; const float CHAT_MESSAGE_SCALE = 0.0015f; const float CHAT_MESSAGE_HEIGHT = 0.1f; -void Avatar::sendAvatarURLsMessage(const QUrl& voxelURL) { - QByteArray message; - - char packetHeader[MAX_PACKET_HEADER_BYTES]; - int numBytesPacketHeader = populateTypeAndVersion((unsigned char*) packetHeader, PACKET_TYPE_AVATAR_URLS); - - message.append(packetHeader, numBytesPacketHeader); - message.append(NodeList::getInstance()->getOwnerUUID().toRfc4122()); - - QDataStream out(&message, QIODevice::WriteOnly | QIODevice::Append); - out << voxelURL; - - Application::controlledBroadcastToNodes((unsigned char*)message.data(), message.size(), &NODE_TYPE_AVATAR_MIXER, 1); -} - Avatar::Avatar(Node* owningNode) : AvatarData(owningNode), _head(this), @@ -359,9 +344,21 @@ bool Avatar::findSphereCollision(const glm::vec3& sphereCenter, float sphereRadi int Avatar::parseData(unsigned char* sourceBuffer, int numBytes) { // change in position implies movement glm::vec3 oldPosition = _position; + + // change in UUID requires mesh and skeleton request to data-server + + QUuid oldUuid = _uuid; + int bytesRead = AvatarData::parseData(sourceBuffer, numBytes); + const float MOVE_DISTANCE_THRESHOLD = 0.001f; _moving = glm::distance(oldPosition, _position) > MOVE_DISTANCE_THRESHOLD; + + if (oldUuid != _uuid && !_uuid.isNull()) { + DataServerClient::getValuesForKeysAndUUID(QStringList() << DataServerKey::FaceMeshURL << DataServerKey::SkeletonURL, + _uuid, Application::getInstance()->getProfile()); + } + return bytesRead; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 3f1da6bc31..725aa9d5cf 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -69,7 +69,6 @@ class Avatar : public AvatarData { Q_OBJECT public: - static void sendAvatarURLsMessage(const QUrl& voxelURL); Avatar(Node* owningNode = NULL); ~Avatar(); diff --git a/interface/src/avatar/Profile.cpp b/interface/src/avatar/Profile.cpp index 316ca195c9..09baac13a9 100644 --- a/interface/src/avatar/Profile.cpp +++ b/interface/src/avatar/Profile.cpp @@ -8,24 +8,29 @@ #include +#include #include +#include "Application.h" #include "Profile.h" -#include "DataServerClient.h" +#include "Util.h" Profile::Profile(const QString &username) : - _username(username), + _username(), _uuid(), _lastDomain(), _lastPosition(0.0, 0.0, 0.0), _lastOrientationSend(0), - _faceModelURL() + _faceModelURL(), + _skeletonModelURL() { - if (!_username.isEmpty()) { + if (!username.isEmpty()) { + setUsername(username); + // we've been given a new username, ask the data-server for profile - DataServerClient::getClientValueForKey(DataServerKey::UUID); - DataServerClient::getClientValueForKey(DataServerKey::FaceMeshURL); - DataServerClient::getClientValueForKey(DataServerKey::SkeletonURL); + DataServerClient::getValueForKeyAndUserString(DataServerKey::UUID, getUserString(), this); + DataServerClient::getValueForKeyAndUserString(DataServerKey::FaceMeshURL, getUserString(), this); + DataServerClient::getValueForKeyAndUserString(DataServerKey::SkeletonURL, getUserString(), this); // send our current domain server to the data-server updateDomain(NodeList::getInstance()->getDomainHostname()); @@ -66,7 +71,7 @@ void Profile::updateDomain(const QString& domain) { _lastDomain = domain; // send the changed domain to the data-server - DataServerClient::putValueForKey(DataServerKey::Domain, domain.toLocal8Bit().constData()); + DataServerClient::putValueForKeyAndUserString(DataServerKey::Domain, domain, getUserString()); } } @@ -95,7 +100,8 @@ void Profile::updatePosition(const glm::vec3 position) { gettimeofday(&lastPositionSend, NULL); // send the changed position to the data-server - DataServerClient::putValueForKey(DataServerKey::Position, createByteArray(position).constData()); + DataServerClient::putValueForKeyAndUserString(DataServerKey::Position, + QString(createByteArray(position)), getUserString()); } } } @@ -111,7 +117,8 @@ void Profile::updateOrientation(const glm::quat& orientation) { uint64_t now = usecTimestampNow(); if (now - _lastOrientationSend >= DATA_SERVER_ORIENTATION_UPDATE_INTERVAL_USECS && glm::distance(_lastOrientation, eulerAngles) >= DATA_SERVER_ORIENTATION_CHANGE_THRESHOLD_DEGREES) { - DataServerClient::putValueForKey(DataServerKey::Orientation, createByteArray(eulerAngles).constData()); + DataServerClient::putValueForKeyAndUserString(DataServerKey::Orientation, QString(createByteArray(eulerAngles)), + getUserString()); _lastOrientation = eulerAngles; _lastOrientationSend = now; @@ -132,10 +139,94 @@ void Profile::saveData(QSettings* settings) { void Profile::loadData(QSettings* settings) { settings->beginGroup("Profile"); - _username = settings->value("username").toString(); + setUsername(settings->value("username").toString()); this->setUUID(settings->value("UUID").toUuid()); _faceModelURL = settings->value("faceModelURL").toUrl(); _skeletonModelURL = settings->value("skeletonModelURL").toUrl(); settings->endGroup(); } + +void Profile::processDataServerResponse(const QString& userString, const QStringList& keyList, const QStringList& valueList) { + for (int i = 0; i < keyList.size(); i++) { + if (valueList[i] != " ") { + if (keyList[i] == DataServerKey::FaceMeshURL) { + if (userString == _username || userString == uuidStringWithoutCurlyBraces(_uuid)) { + qDebug("Changing user's face model URL to %s", valueList[i].toLocal8Bit().constData()); + Application::getInstance()->getProfile()->setFaceModelURL(QUrl(valueList[i])); + } else { + // mesh URL for a UUID, find avatar in our list + + foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { + if (node->getLinkedData() != NULL && node->getType() == NODE_TYPE_AGENT) { + Avatar* avatar = (Avatar *) node->getLinkedData(); + + if (avatar->getUUID() == QUuid(userString)) { + QMetaObject::invokeMethod(&avatar->getHead().getFaceModel(), + "setURL", Q_ARG(QUrl, QUrl(valueList[i]))); + } + } + } + } + } else if (keyList[i] == DataServerKey::SkeletonURL) { + if (userString == _username || userString == uuidStringWithoutCurlyBraces(_uuid)) { + qDebug("Changing user's skeleton URL to %s", valueList[i].toLocal8Bit().constData()); + Application::getInstance()->getProfile()->setSkeletonModelURL(QUrl(valueList[i])); + } else { + // skeleton URL for a UUID, find avatar in our list + foreach (const SharedNodePointer& node, NodeList::getInstance()->getNodeHash()) { + if (node->getLinkedData() != NULL && node->getType() == NODE_TYPE_AGENT) { + Avatar* avatar = (Avatar *) node->getLinkedData(); + + if (avatar->getUUID() == QUuid(userString)) { + QMetaObject::invokeMethod(&avatar->getSkeletonModel(), "setURL", + Q_ARG(QUrl, QUrl(valueList[i]))); + } + } + } + } + } else if (keyList[i] == DataServerKey::Domain && keyList[i + 1] == DataServerKey::Position && + keyList[i + 2] == DataServerKey::Orientation && valueList[i] != " " && + valueList[i + 1] != " " && valueList[i + 2] != " ") { + + QStringList coordinateItems = valueList[i + 1].split(','); + QStringList orientationItems = valueList[i + 2].split(','); + + if (coordinateItems.size() == 3 && orientationItems.size() == 3) { + + // send a node kill request, indicating to other clients that they should play the "disappeared" effect + NodeList::getInstance()->sendKillNode(&NODE_TYPE_AVATAR_MIXER, 1); + + qDebug() << "Changing domain to" << valueList[i].toLocal8Bit().constData() << + ", position to" << valueList[i + 1].toLocal8Bit().constData() << + ", and orientation to" << valueList[i + 2].toLocal8Bit().constData() << + "to go to" << userString; + + NodeList::getInstance()->setDomainHostname(valueList[i]); + // orient the user to face the target + glm::quat newOrientation = glm::quat(glm::radians(glm::vec3(orientationItems[0].toFloat(), + orientationItems[1].toFloat(), + orientationItems[2].toFloat()))) * + glm::angleAxis(180.0f, 0.0f, 1.0f, 0.0f); + Application::getInstance()->getAvatar() + ->setOrientation(newOrientation); + + // move the user a couple units away + const float DISTANCE_TO_USER = 2.0f; + glm::vec3 newPosition = glm::vec3(coordinateItems[0].toFloat(), coordinateItems[1].toFloat(), + coordinateItems[2].toFloat() + ) - newOrientation * IDENTITY_FRONT * DISTANCE_TO_USER; + Application::getInstance()->getAvatar()->setPosition(newPosition); + } + + } else if (keyList[i] == DataServerKey::UUID) { + // this is the user's UUID - set it on the profile + _uuid = QUuid(valueList[i]); + } + } + } +} + +void Profile::setUsername(const QString& username) { + _username = username; +} diff --git a/interface/src/avatar/Profile.h b/interface/src/avatar/Profile.h index 06f8de94a3..225022b99d 100644 --- a/interface/src/avatar/Profile.h +++ b/interface/src/avatar/Profile.h @@ -18,7 +18,9 @@ #include #include -class Profile { +#include "DataServerClient.h" + +class Profile : public DataServerCallbackObject { public: Profile(const QString& username); @@ -44,7 +46,12 @@ public: void saveData(QSettings* settings); void loadData(QSettings* settings); + + void processDataServerResponse(const QString& userString, const QStringList& keyList, const QStringList& valueList); private: + + void setUsername(const QString& username); + QString _username; QUuid _uuid; QString _lastDomain; diff --git a/interface/src/ui/UpdateDialog.cpp b/interface/src/ui/UpdateDialog.cpp new file mode 100644 index 0000000000..12c5cd83e0 --- /dev/null +++ b/interface/src/ui/UpdateDialog.cpp @@ -0,0 +1,61 @@ +// +// UpdateDialog.cpp +// interface +// +// Created by Leonardo Murillo on 1/8/14. +// Copyright (c) 2013, 2014 High Fidelity, Inc. All rights reserved. +// + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "SharedUtil.h" +#include "UpdateDialog.h" + +UpdateDialog::UpdateDialog(QWidget *parent, const QString& releaseNotes, const QString& latestVersion, const QUrl& downloadURL) : + QWidget(parent, Qt::Widget), + _latestVersion(latestVersion), + _downloadUrl(downloadURL) { + + QUiLoader updateDialogLoader; + QWidget* updateDialog; + QFile updateDialogUi("resources/ui/updateDialog.ui"); + updateDialogUi.open(QFile::ReadOnly); + updateDialog = updateDialogLoader.load(&updateDialogUi, this); + + QString updateRequired = QString("You are currently running build %1, the latest build released is %2. \ + Please download and install the most recent release to access the latest features and bug fixes.") + .arg(Application::getInstance()->applicationVersion(), latestVersion); + + + updateDialog->setAttribute(Qt::WA_DeleteOnClose); + + QPushButton* downloadButton = updateDialog->findChild("downloadButton"); + QPushButton* skipButton = updateDialog->findChild("skipButton"); + QPushButton* closeButton = updateDialog->findChild("closeButton"); + QLabel* updateContent = updateDialog->findChild("updateContent"); + + updateContent->setText(updateRequired); + + connect(downloadButton, SIGNAL(released()), this, SLOT(handleDownload())); + connect(skipButton, SIGNAL(released()), this, SLOT(handleSkip())); + connect(closeButton, SIGNAL(released()), this, SLOT(close())); + updateDialog->show(); +} + +void UpdateDialog::handleDownload() { + QDesktopServices::openUrl(_downloadUrl); + Application::getInstance()->quit(); +} + +void UpdateDialog::handleSkip() { + Application::getInstance()->skipVersion(_latestVersion); + this->close(); +} \ No newline at end of file diff --git a/interface/src/ui/UpdateDialog.h b/interface/src/ui/UpdateDialog.h new file mode 100644 index 0000000000..1c4f5de9d0 --- /dev/null +++ b/interface/src/ui/UpdateDialog.h @@ -0,0 +1,29 @@ +// +// UpdateDialog.h +// interface +// +// Created by Leonardo Murillo on 1/8/14. +// Copyright (c) 2013, 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __hifi__UpdateDialog__ +#define __hifi__UpdateDialog__ + +#include + +class UpdateDialog : public QWidget { + Q_OBJECT + +public: + UpdateDialog(QWidget* parent, const QString& releaseNotes, const QString& latestVersion, const QUrl& downloadURL); + +private: + QString _latestVersion; + QUrl _downloadUrl; + +private slots: + void handleDownload(); + void handleSkip(); +}; + +#endif /* defined(__hifi__UpdateDialog__) */ diff --git a/libraries/particles/src/ParticleTreeElement.cpp b/libraries/particles/src/ParticleTreeElement.cpp index 5dcae129b7..7994909004 100644 --- a/libraries/particles/src/ParticleTreeElement.cpp +++ b/libraries/particles/src/ParticleTreeElement.cpp @@ -80,9 +80,7 @@ void ParticleTreeElement::update(ParticleTreeUpdateArgs& args) { // erase this particle particleItr = _particles->erase(particleItr); - } - else - { + } else { ++particleItr; } } diff --git a/libraries/script-engine/src/DataServerScriptingInterface.cpp b/libraries/script-engine/src/DataServerScriptingInterface.cpp new file mode 100644 index 0000000000..236f46e54c --- /dev/null +++ b/libraries/script-engine/src/DataServerScriptingInterface.cpp @@ -0,0 +1,21 @@ +// +// DataServerScriptingInterface.cpp +// hifi +// +// Created by Stephen Birarda on 1/20/2014. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// + +#include + +#include "DataServerScriptingInterface.h" + +DataServerScriptingInterface::DataServerScriptingInterface() : + _uuid(QUuid::createUuid()) +{ + +} + +void DataServerScriptingInterface::setValueForKey(const QString& key, const QString& value) { + DataServerClient::putValueForKeyAndUUID(key, value, _uuid); +} \ No newline at end of file diff --git a/libraries/script-engine/src/DataServerScriptingInterface.h b/libraries/script-engine/src/DataServerScriptingInterface.h new file mode 100644 index 0000000000..8bd3eea565 --- /dev/null +++ b/libraries/script-engine/src/DataServerScriptingInterface.h @@ -0,0 +1,30 @@ +// +// DataServerScriptingInterface.h +// hifi +// +// Created by Stephen Birarda on 1/20/2014. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// + +#ifndef __hifi__DataServerScriptingInterface__ +#define __hifi__DataServerScriptingInterface__ + +#include +#include +#include + +class DataServerScriptingInterface : public QObject { + Q_OBJECT +public: + DataServerScriptingInterface(); + + void refreshUUID() { _uuid = QUuid::createUuid(); } + const QUuid& getUUID() const { return _uuid; } + +public slots: + void setValueForKey(const QString& key, const QString& value); +private: + QUuid _uuid; +}; + +#endif /* defined(__hifi__DataServerScriptingInterface__) */ diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 34f11de0b6..3b880d99d3 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -38,17 +38,23 @@ static QScriptValue soundConstructor(QScriptContext* context, QScriptEngine* eng return soundScriptValue; } -ScriptEngine::ScriptEngine(const QString& scriptContents, bool wantMenuItems, - const char* scriptMenuName, AbstractMenuInterface* menu, - AbstractControllerScriptingInterface* controllerScriptingInterface) { + +ScriptEngine::ScriptEngine(const QString& scriptContents, bool wantMenuItems, const QString& fileNameString, AbstractMenuInterface* menu, + AbstractControllerScriptingInterface* controllerScriptingInterface) : + _avatarData(NULL) +{ _scriptContents = scriptContents; _isFinished = false; _isRunning = false; _isInitialized = false; + _fileNameString = fileNameString; + + QByteArray fileNameAscii = fileNameString.toLocal8Bit(); + const char* scriptMenuName = fileNameAscii.data(); // some clients will use these menu features _wantMenuItems = wantMenuItems; - if (scriptMenuName) { + if (!fileNameString.isEmpty()) { _scriptMenuName = "Stop "; _scriptMenuName.append(scriptMenuName); _scriptMenuName.append(QString(" [%1]").arg(_scriptNumber)); @@ -65,6 +71,15 @@ ScriptEngine::~ScriptEngine() { //printf("ScriptEngine::~ScriptEngine()...\n"); } +void ScriptEngine::setAvatarData(AvatarData* avatarData) { + _avatarData = avatarData; + + // remove the old Avatar property, if it exists + _engine.globalObject().setProperty("Avatar", QScriptValue()); + + // give the script engine the new Avatar script property + registerGlobalObject("Avatar", _avatarData); +} void ScriptEngine::setupMenuItems() { if (_menu && _wantMenuItems) { @@ -121,6 +136,8 @@ void ScriptEngine::init() { QScriptValue audioScriptingInterfaceValue = _engine.newQObject(&_audioScriptingInterface); _engine.globalObject().setProperty("Audio", audioScriptingInterfaceValue); + + registerGlobalObject("Data", &_dataServerScriptingInterface); if (_controllerScriptingInterface) { QScriptValue controllerScripterValue = _engine.newQObject(_controllerScriptingInterface); @@ -142,6 +159,10 @@ void ScriptEngine::registerGlobalObject(const QString& name, QObject* object) { _engine.globalObject().setProperty(name, value); } +void ScriptEngine::preEvaluateReset() { + _dataServerScriptingInterface.refreshUUID(); +} + void ScriptEngine::evaluate() { if (!_isInitialized) { init(); @@ -171,6 +192,8 @@ void ScriptEngine::run() { gettimeofday(&startTime, NULL); int thisFrame = 0; + + NodeList* nodeList = NodeList::getInstance(); while (!_isFinished) { int usecToSleep = usecTimestamp(&startTime) + (thisFrame++ * VISUAL_DATA_CALLBACK_USECS) - usecTimestampNow(); @@ -214,6 +237,26 @@ void ScriptEngine::run() { _particlesScriptingInterface.getParticlePacketSender()->process(); } } + + if (_isAvatar && _avatarData) { + static unsigned char avatarPacket[MAX_PACKET_SIZE]; + static int numAvatarHeaderBytes = 0; + + if (numAvatarHeaderBytes == 0) { + // pack the avatar header bytes the first time + // unlike the _avatar.getBroadcastData these won't change + numAvatarHeaderBytes = populateTypeAndVersion(avatarPacket, PACKET_TYPE_HEAD_DATA); + + // pack the owner UUID for this script + QByteArray ownerUUID = nodeList->getOwnerUUID().toRfc4122(); + memcpy(avatarPacket + numAvatarHeaderBytes, ownerUUID.constData(), ownerUUID.size()); + numAvatarHeaderBytes += ownerUUID.size(); + } + + int numAvatarPacketBytes = _avatarData->getBroadcastData(avatarPacket + numAvatarHeaderBytes) + numAvatarHeaderBytes; + + nodeList->broadcastToNodes(avatarPacket, numAvatarPacketBytes, &NODE_TYPE_AVATAR_MIXER, 1); + } if (willSendVisualDataCallBack) { emit willSendVisualDataCallback(); @@ -232,9 +275,11 @@ void ScriptEngine::run() { thread()->quit(); } - emit finished(); + emit finished(_fileNameString); + _isRunning = false; } + void ScriptEngine::stop() { _isFinished = true; } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 06597f82a8..8be3883f39 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -19,17 +19,22 @@ #include #include +#include + class ParticlesScriptingInterface; #include "AbstractControllerScriptingInterface.h" +#include "DataServerScriptingInterface.h" const QString NO_SCRIPT(""); class ScriptEngine : public QObject { Q_OBJECT + + Q_PROPERTY(bool isAvatar READ isAvatar WRITE setIsAvatar) public: ScriptEngine(const QString& scriptContents = NO_SCRIPT, bool wantMenuItems = false, - const char* scriptMenuName = NULL, AbstractMenuInterface* menu = NULL, + const QString& scriptMenuName = QString(""), AbstractMenuInterface* menu = NULL, AbstractControllerScriptingInterface* controllerScriptingInterface = NULL); ~ScriptEngine(); @@ -39,6 +44,9 @@ public: /// Access the ParticlesScriptingInterface in order to initialize it with a custom packet sender and jurisdiction listener ParticlesScriptingInterface* getParticlesScriptingInterface() { return &_particlesScriptingInterface; } + + /// Access the DataServerScriptingInterface for access to its underlying UUID + const DataServerScriptingInterface& getDataServerScriptingInterface() { return _dataServerScriptingInterface; } /// sets the script contents, will return false if failed, will fail if script is already running bool setScriptContents(const QString& scriptContents); @@ -47,6 +55,11 @@ public: void cleanMenuItems(); void registerGlobalObject(const QString& name, QObject* object); /// registers a global object by name + + void setIsAvatar(bool isAvatar) { _isAvatar = isAvatar; } + bool isAvatar() const { return _isAvatar; } + + void setAvatarData(AvatarData* avatarData); public slots: void init(); @@ -58,22 +71,28 @@ signals: void willSendAudioDataCallback(); void willSendVisualDataCallback(); void scriptEnding(); - void finished(); + void finished(const QString& fileNameString); protected: + void preEvaluateReset(); + QString _scriptContents; bool _isFinished; bool _isRunning; bool _isInitialized; QScriptEngine _engine; + bool _isAvatar; private: static VoxelsScriptingInterface _voxelsScriptingInterface; static ParticlesScriptingInterface _particlesScriptingInterface; AbstractControllerScriptingInterface* _controllerScriptingInterface; AudioScriptingInterface _audioScriptingInterface; + DataServerScriptingInterface _dataServerScriptingInterface; + AvatarData* _avatarData; bool _wantMenuItems; QString _scriptMenuName; + QString _fileNameString; AbstractMenuInterface* _menu; static int _scriptNumber; }; diff --git a/libraries/shared/src/DataServerClient.cpp b/libraries/shared/src/DataServerClient.cpp new file mode 100644 index 0000000000..a7a213f42c --- /dev/null +++ b/libraries/shared/src/DataServerClient.cpp @@ -0,0 +1,190 @@ +// +// DataServerClient.cpp +// hifi +// +// Created by Stephen Birarda on 10/7/13. +// Copyright (c) 2013 HighFidelity, Inc. All rights reserved. +// + +#include +#include + +#include "NodeList.h" +#include "PacketHeaders.h" +#include "UUID.h" + +#include "DataServerClient.h" + +QMap DataServerClient::_unmatchedPackets; +QMap DataServerClient::_callbackObjects; +quint8 DataServerClient::_sequenceNumber = 0; + +const char MULTI_KEY_VALUE_SEPARATOR = '|'; + +const char DATA_SERVER_HOSTNAME[] = "localhost"; +const unsigned short DATA_SERVER_PORT = 3282; + +const HifiSockAddr& DataServerClient::dataServerSockAddr() { + static HifiSockAddr dsSockAddr = HifiSockAddr(DATA_SERVER_HOSTNAME, DATA_SERVER_PORT); + return dsSockAddr; +} + +void DataServerClient::putValueForKeyAndUserString(const QString& key, const QString& value, const QString& userString) { + unsigned char putPacket[MAX_PACKET_SIZE]; + + // setup the header for this packet + int numPacketBytes = populateTypeAndVersion(putPacket, PACKET_TYPE_DATA_SERVER_PUT); + + // pack the sequence number + memcpy(putPacket + numPacketBytes, &_sequenceNumber, sizeof(_sequenceNumber)); + numPacketBytes += sizeof(_sequenceNumber); + + // pack the client UUID, null terminated + memcpy(putPacket + numPacketBytes, qPrintable(userString), userString.size()); + numPacketBytes += userString.size(); + putPacket[numPacketBytes++] = '\0'; + + // pack a 1 to designate that we are putting a single value + putPacket[numPacketBytes++] = 1; + + // pack the key, null terminated + strcpy((char*) putPacket + numPacketBytes, qPrintable(key)); + numPacketBytes += key.size(); + putPacket[numPacketBytes++] = '\0'; + + // pack the value, null terminated + strcpy((char*) putPacket + numPacketBytes, qPrintable(value)); + numPacketBytes += value.size(); + putPacket[numPacketBytes++] = '\0'; + + // add the putPacket to our vector of unconfirmed packets, will be deleted once put is confirmed + _unmatchedPackets.insert(_sequenceNumber, QByteArray((char*) putPacket, numPacketBytes)); + + // send this put request to the data server + NodeList::getInstance()->getNodeSocket().writeDatagram((char*) putPacket, numPacketBytes, + dataServerSockAddr().getAddress(), + dataServerSockAddr().getPort()); + + // push the sequence number forwards + _sequenceNumber++; +} + +void DataServerClient::putValueForKeyAndUUID(const QString& key, const QString& value, const QUuid& uuid) { + putValueForKeyAndUserString(key, value, uuidStringWithoutCurlyBraces(uuid)); +} + +void DataServerClient::getValueForKeyAndUUID(const QString& key, const QUuid &uuid, DataServerCallbackObject* callbackObject) { + getValuesForKeysAndUUID(QStringList(key), uuid, callbackObject); +} + +void DataServerClient::getValuesForKeysAndUUID(const QStringList& keys, const QUuid& uuid, + DataServerCallbackObject* callbackObject) { + if (!uuid.isNull()) { + getValuesForKeysAndUserString(keys, uuidStringWithoutCurlyBraces(uuid), callbackObject); + } +} + +void DataServerClient::getValuesForKeysAndUserString(const QStringList& keys, const QString& userString, + DataServerCallbackObject* callbackObject) { + if (!userString.isEmpty() && keys.size() <= UCHAR_MAX) { + unsigned char getPacket[MAX_PACKET_SIZE]; + + // setup the header for this packet + int numPacketBytes = populateTypeAndVersion(getPacket, PACKET_TYPE_DATA_SERVER_GET); + + // pack the sequence number + memcpy(getPacket + numPacketBytes, &_sequenceNumber, sizeof(_sequenceNumber)); + numPacketBytes += sizeof(_sequenceNumber); + + // pack the user string (could be username or UUID string), null-terminate + memcpy(getPacket + numPacketBytes, qPrintable(userString), userString.size()); + numPacketBytes += userString.size(); + getPacket[numPacketBytes++] = '\0'; + + // pack one byte to designate the number of keys + getPacket[numPacketBytes++] = keys.size(); + + QString keyString = keys.join(MULTI_KEY_VALUE_SEPARATOR); + + // pack the key string, null terminated + strcpy((char*) getPacket + numPacketBytes, keyString.toLocal8Bit().constData()); + numPacketBytes += keyString.size() + sizeof('\0'); + + // add the getPacket to our map of unconfirmed packets, will be deleted once we get a response from the nameserver + _unmatchedPackets.insert(_sequenceNumber, QByteArray((char*) getPacket, numPacketBytes)); + _callbackObjects.insert(_sequenceNumber, callbackObject); + + // send the get to the data server + NodeList::getInstance()->getNodeSocket().writeDatagram((char*) getPacket, numPacketBytes, + dataServerSockAddr().getAddress(), + dataServerSockAddr().getPort()); + + _sequenceNumber++; + } +} + +void DataServerClient::getValueForKeyAndUserString(const QString& key, const QString& userString, + DataServerCallbackObject* callbackObject) { + getValuesForKeysAndUserString(QStringList(key), userString, callbackObject); +} + +void DataServerClient::processConfirmFromDataServer(unsigned char* packetData) { + removeMatchedPacketFromMap(packetData); +} + +void DataServerClient::processSendFromDataServer(unsigned char* packetData, int numPacketBytes) { + // pull the user string from the packet so we know who to associate this with + int numHeaderBytes = numBytesForPacketHeader(packetData); + + quint8 sequenceNumber = *(packetData + numHeaderBytes); + + if (_callbackObjects.find(sequenceNumber) != _callbackObjects.end()) { + // remove the packet from our two maps, it's matched + DataServerCallbackObject* callbackObject = _callbackObjects.take(sequenceNumber); + _unmatchedPackets.remove(sequenceNumber); + + char* userStringPosition = (char*) packetData + numHeaderBytes + sizeof(sequenceNumber); + QString userString(userStringPosition); + + char* keysPosition = userStringPosition + userString.size() + sizeof('\0') + sizeof(unsigned char); + char* valuesPosition = keysPosition + strlen(keysPosition) + sizeof('\0'); + + QStringList keyList = QString(keysPosition).split(MULTI_KEY_VALUE_SEPARATOR); + QStringList valueList = QString(valuesPosition).split(MULTI_KEY_VALUE_SEPARATOR); + + callbackObject->processDataServerResponse(userString, keyList, valueList); + } +} + +void DataServerClient::processMessageFromDataServer(unsigned char* packetData, int numPacketBytes) { + switch (packetData[0]) { + case PACKET_TYPE_DATA_SERVER_SEND: + processSendFromDataServer(packetData, numPacketBytes); + break; + case PACKET_TYPE_DATA_SERVER_CONFIRM: + processConfirmFromDataServer(packetData); + break; + default: + break; + } +} + +void DataServerClient::removeMatchedPacketFromMap(unsigned char* packetData) { + quint8 sequenceNumber = *(packetData + numBytesForPacketHeader(packetData)); + + // attempt to remove a packet with this sequence number from the QMap of unmatched packets + _unmatchedPackets.remove(sequenceNumber); +} + +void DataServerClient::resendUnmatchedPackets() { + if (_unmatchedPackets.size() > 0) { + qDebug() << "Resending" << _unmatchedPackets.size() << "packets to the data server."; + + foreach (const QByteArray& packet, _unmatchedPackets) { + // send the unmatched packet to the data server + NodeList::getInstance()->getNodeSocket().writeDatagram(packet.data(), packet.size(), + dataServerSockAddr().getAddress(), + dataServerSockAddr().getPort()); + } + } +} diff --git a/interface/src/DataServerClient.h b/libraries/shared/src/DataServerClient.h similarity index 53% rename from interface/src/DataServerClient.h rename to libraries/shared/src/DataServerClient.h index 89ec944eb6..78826022ae 100644 --- a/interface/src/DataServerClient.h +++ b/libraries/shared/src/DataServerClient.h @@ -11,29 +11,43 @@ #include +#include +#include +#include #include -#include "Application.h" +#include "HifiSockAddr.h" + +class DataServerCallbackObject { +public: + virtual void processDataServerResponse(const QString& userString, const QStringList& keyList, const QStringList& valueList) = 0; +}; class DataServerClient { public: static const HifiSockAddr& dataServerSockAddr(); - static void putValueForKey(const QString& key, const char* value); - static void getClientValueForKey(const QString& key); - static void getValueForKeyAndUUID(const QString& key, const QUuid& uuid); - static void getValuesForKeysAndUUID(const QStringList& keys, const QUuid& uuid); - static void getValuesForKeysAndUserString(const QStringList& keys, const QString& userString); + static void putValueForKeyAndUserString(const QString& key, const QString& value, const QString& userString); + static void putValueForKeyAndUUID(const QString& key, const QString& value, const QUuid& uuid); + + static void getValueForKeyAndUserString(const QString& key, const QString& userString, + DataServerCallbackObject* callbackObject); + static void getValueForKeyAndUUID(const QString& key, const QUuid& uuid, DataServerCallbackObject* callbackObject); + static void getValuesForKeysAndUUID(const QStringList& keys, const QUuid& uuid, DataServerCallbackObject* callbackObject); + static void getValuesForKeysAndUserString(const QStringList& keys, const QString& userString, + DataServerCallbackObject* callbackObject); - static void processConfirmFromDataServer(unsigned char* packetData, int numPacketBytes); - static void processSendFromDataServer(unsigned char* packetData, int numPacketBytes); static void processMessageFromDataServer(unsigned char* packetData, int numPacketBytes); - static void removeMatchedPacketFromMap(unsigned char* packetData, int numPacketBytes); + static void resendUnmatchedPackets(); private: + static void processConfirmFromDataServer(unsigned char* packetData); + static void processSendFromDataServer(unsigned char* packetData, int numPacketBytes); + static void removeMatchedPacketFromMap(unsigned char* packetData); - - static std::map _unmatchedPackets; + static QMap _unmatchedPackets; + static QMap _callbackObjects; + static quint8 _sequenceNumber; }; namespace DataServerKey { diff --git a/libraries/shared/src/PacketHeaders.cpp b/libraries/shared/src/PacketHeaders.cpp index 2d9ffbcf39..b475c9febc 100644 --- a/libraries/shared/src/PacketHeaders.cpp +++ b/libraries/shared/src/PacketHeaders.cpp @@ -22,9 +22,6 @@ PACKET_VERSION versionForPacketType(PACKET_TYPE type) { case PACKET_TYPE_HEAD_DATA: return 15; - case PACKET_TYPE_AVATAR_URLS: - return 2; - case PACKET_TYPE_OCTREE_STATS: return 2; @@ -56,6 +53,12 @@ PACKET_VERSION versionForPacketType(PACKET_TYPE type) { case PACKET_TYPE_PING_REPLY: return 1; + case PACKET_TYPE_DATA_SERVER_GET: + case PACKET_TYPE_DATA_SERVER_PUT: + case PACKET_TYPE_DATA_SERVER_SEND: + case PACKET_TYPE_DATA_SERVER_CONFIRM: + return 1; + default: return 0; } diff --git a/libraries/shared/src/PacketHeaders.h b/libraries/shared/src/PacketHeaders.h index f4fe5cd0a8..5d1c657d6b 100644 --- a/libraries/shared/src/PacketHeaders.h +++ b/libraries/shared/src/PacketHeaders.h @@ -25,7 +25,6 @@ const PACKET_TYPE PACKET_TYPE_MIXED_AUDIO = 'A'; const PACKET_TYPE PACKET_TYPE_MICROPHONE_AUDIO_NO_ECHO = 'M'; const PACKET_TYPE PACKET_TYPE_MICROPHONE_AUDIO_WITH_ECHO = 'm'; const PACKET_TYPE PACKET_TYPE_BULK_AVATAR_DATA = 'X'; -const PACKET_TYPE PACKET_TYPE_AVATAR_URLS = 'U'; const PACKET_TYPE PACKET_TYPE_TRANSMITTER_DATA_V2 = 'T'; const PACKET_TYPE PACKET_TYPE_ENVIRONMENT_DATA = 'e'; const PACKET_TYPE PACKET_TYPE_DOMAIN_LIST_REQUEST = 'L';