aboutsummaryrefslogtreecommitdiff
path: root/jim-redis.c
blob: 8722e9f228087cd43b147bd07dda77eee509bac3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
/*
 * Simple redis interface
 *
 * (c) 2020 Steve Bennett <steveb@workware.net.au>
 *
 * See LICENSE for license details.
 */
#include <jim.h>
#include <jim-eventloop.h>
#include <unistd.h>
#include <hiredis.h>

/**
 * Recursively decode a redis reply as Tcl data structure.
 */
static Jim_Obj *jim_redis_get_result(Jim_Interp *interp, redisReply *reply)
{
    int i;
    switch (reply->type) {
        case REDIS_REPLY_INTEGER:
            return Jim_NewIntObj(interp, reply->integer);
        case REDIS_REPLY_STATUS:
        case REDIS_REPLY_ERROR:
        case REDIS_REPLY_STRING:
            return Jim_NewStringObj(interp, reply->str, reply->len);
            break;
        case REDIS_REPLY_ARRAY:
            {
                Jim_Obj *obj = Jim_NewListObj(interp, NULL, 0);
                for (i = 0; i < reply->elements; i++) {
                    Jim_ListAppendElement(interp, obj, jim_redis_get_result(interp, reply->element[i]));
                }
                return obj;
            }
        case REDIS_REPLY_NIL:
            return Jim_NewStringObj(interp, NULL, 0);
        default:
            return Jim_NewStringObj(interp, "badtype", -1);
    }
}

/**
 * $r readable ?script?
 * - set or clear a readable script
 * $r close
 * - close (delete) the handle
 * $r read
 * - synchronously read a SUBSCRIBE response (typically from within readable)
 * $r <redis-command> ...
 * - invoke the redis command and return the decoded result
 */
static int jim_redis_subcmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    int i;
    redisContext *c = Jim_CmdPrivData(interp);
    const char **args;
    size_t *arglens;
    int ret = JIM_OK;

    redisReply *reply;

    if (argc < 2) {
        Jim_WrongNumArgs(interp, 1, argv, "cmd ?args ...?");
        return JIM_ERR;
    }

    if (Jim_CompareStringImmediate(interp, argv[1], "readable")) {
        /* Remove any existing handler */
        Jim_DeleteFileHandler(interp, c->fd, JIM_EVENT_READABLE);
        if (argc > 2) {
            Jim_CreateScriptFileHandler(interp, c->fd, JIM_EVENT_READABLE, argv[2]);
        }
        return JIM_OK;
    }
    if (Jim_CompareStringImmediate(interp, argv[1], "close")) {
        return Jim_DeleteCommand(interp, argv[0]);
    }
    if (Jim_CompareStringImmediate(interp, argv[1], "read")) {
        if (redisGetReply(c, (void **)&reply) != REDIS_OK) {
            reply = NULL;
        }
    }
    else {
        args = Jim_Alloc(sizeof(*args) * argc - 1);
        arglens = Jim_Alloc(sizeof(*arglens) * argc - 1);
        for (i = 1; i < argc; i++) {
            args[i - 1] = Jim_String(argv[i]);
            arglens[i - 1] = Jim_Length(argv[i]);
        }
        reply = redisCommandArgv(c, argc - 1, args, arglens);
        Jim_Free(args);
        Jim_Free(arglens);
    }
    /* sometimes commands return NULL */
    if (reply) {
        Jim_SetResult(interp, jim_redis_get_result(interp, reply));
        if (reply->type == REDIS_REPLY_ERROR) {
            ret = JIM_ERR;
        }
        freeReplyObject(reply);
    }
    else if (c->err) {
        Jim_SetResultFormatted(interp, "%#s: %s", argv[1], c->errstr);
        ret = JIM_ERR;
    }
    return ret;
}

static void jim_redis_del_proc(Jim_Interp *interp, void *privData)
{
    redisContext *c = privData;
    JIM_NOTUSED(interp);
    Jim_DeleteFileHandler(interp, c->fd, JIM_EVENT_READABLE);
    redisFree(c);
}

/**
 * redis <socket-stream>
 *
 * Returns a handle that can be used to communicate with the redis
 * instance over the socket.
 * The original socket handle is closed.
 */
static int jim_redis_cmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    redisContext *c;
    char buf[60];
    Jim_Obj *objv[2];
    long fd;
    int ret;

    if (argc != 2) {
        Jim_WrongNumArgs(interp, 1, argv, "socket-stream");
        return JIM_ERR;
    }

    /* Invoke getfd to get the file descriptor */
    objv[0] = argv[1];
    objv[1] = Jim_NewStringObj(interp, "getfd", -1);
    ret = Jim_EvalObjVector(interp, 2, objv);
    if (ret == JIM_OK) {
        ret = Jim_GetLong(interp, Jim_GetResult(interp), &fd) == JIM_ERR;
    }
    if (ret != JIM_OK) {
        Jim_SetResultFormatted(interp, "%#s: not a valid stream handle: %#s", argv[0], argv[1]);
        return ret;
    }

    /* Note that we dup the file descriptor here so that we can close the original */
    fd = dup(fd);
    /* Can't fail */
    c = redisConnectFd(fd);
    /* Now delete the original stream */
    Jim_DeleteCommand(interp, argv[1]);
    snprintf(buf, sizeof(buf), "redis.handle%ld", Jim_GetId(interp));
    Jim_CreateCommand(interp, buf, jim_redis_subcmd, c, jim_redis_del_proc);

    Jim_SetResult(interp, Jim_MakeGlobalNamespaceName(interp, Jim_NewStringObj(interp, buf, -1)));

    return JIM_OK;
}

int
Jim_redisInit(Jim_Interp *interp)
{
    if (Jim_PackageProvide(interp, "redis", "1.0", JIM_ERRMSG))
        return JIM_ERR;

    Jim_CreateCommand(interp, "redis", jim_redis_cmd, NULL, NULL);
    return JIM_OK;
}