c5a899a1ff1b0550b24c2fa8831d81913d24013c
[rrdtool.git] / bindings / tcl / tclrrd.c
1 /*
2  * tclrrd.c -- A TCL interpreter extension to access the RRD library.
3  *
4  * Copyright (c) 1999,2000 Frank Strauss, Technical University of Braunschweig.
5  *
6  * Thread-safe code copyright (c) 2005 Oleg Derevenetz, CenterTelecom Voronezh ISP.
7  *
8  * See the file "COPYING" for information on usage and redistribution
9  * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
10  *
11  * $Id$
12  */
13
14
15
16 #include <errno.h>
17 #include <string.h>
18 #include <time.h>
19 #include <unistd.h>
20 #include <tcl.h>
21 #include <stdlib.h>
22 #include "../../src/rrd_tool.h"
23 #include "../../src/rrd_format.h"
24
25 /* support pre-8.4 tcl */
26
27 #ifndef CONST84
28 #   define CONST84
29 #endif
30
31 extern int Tclrrd_Init(
32     Tcl_Interp *interp);
33 extern int Tclrrd_SafeInit(
34     Tcl_Interp *interp);
35
36
37 /*
38  * some rrd_XXX() and new thread-safe versions of Rrd_XXX()
39  * functions might modify the argv strings passed to it.
40  * Hence, we need to do some preparation before
41  * calling the rrd library functions.
42  */
43 static char **getopt_init(
44     int argc,
45     CONST84 char *argv[])
46 {
47     char    **argv2;
48     int       i;
49
50     argv2 = calloc(argc, sizeof(char *));
51     for (i = 0; i < argc; i++) {
52         argv2[i] = strdup(argv[i]);
53     }
54     return argv2;
55 }
56
57 static void getopt_cleanup(
58     int argc,
59     char **argv2)
60 {
61     int       i;
62
63     for (i = 0; i < argc; i++) {
64         if (argv2[i] != NULL) {
65             free(argv2[i]);
66         }
67     }
68     free(argv2);
69 }
70
71 static void getopt_free_element(
72     argv2,
73     argn)
74     char     *argv2[];
75     int argn;
76 {
77     if (argv2[argn] != NULL) {
78         free(argv2[argn]);
79         argv2[argn] = NULL;
80     }
81 }
82
83 static void getopt_squieeze(
84     argc,
85     argv2)
86     int      *argc;
87     char     *argv2[];
88 {
89     int       i, null_i = 0, argc_tmp = *argc;
90
91     for (i = 0; i < argc_tmp; i++) {
92         if (argv2[i] == NULL) {
93             (*argc)--;
94         } else {
95             argv2[null_i++] = argv2[i];
96         }
97     }
98 }
99
100
101
102 /* Thread-safe version */
103 static int Rrd_Create(
104     ClientData clientData,
105     Tcl_Interp *interp,
106     int argc,
107     CONST84 char *argv[])
108 {
109     int       argv_i;
110     char    **argv2;
111     char     *parsetime_error = NULL;
112     time_t    last_up = time(NULL) - 10;
113     long int  long_tmp;
114     unsigned long int pdp_step = 300;
115     struct rrd_time_value last_up_tv;
116
117     argv2 = getopt_init(argc, argv);
118
119     for (argv_i = 1; argv_i < argc; argv_i++) {
120         if (!strcmp(argv2[argv_i], "--start") || !strcmp(argv2[argv_i], "-b")) {
121             if (argv_i++ >= argc) {
122                 Tcl_AppendResult(interp, "RRD Error: option '",
123                                  argv2[argv_i - 1], "' needs an argument",
124                                  (char *) NULL);
125                 getopt_cleanup(argc, argv2);
126                 return TCL_ERROR;
127             }
128             if ((parsetime_error = parsetime(argv2[argv_i], &last_up_tv))) {
129                 Tcl_AppendResult(interp, "RRD Error: invalid time format: '",
130                                  argv2[argv_i], "'", (char *) NULL);
131                 getopt_cleanup(argc, argv2);
132                 return TCL_ERROR;
133             }
134             if (last_up_tv.type == RELATIVE_TO_END_TIME ||
135                 last_up_tv.type == RELATIVE_TO_START_TIME) {
136                 Tcl_AppendResult(interp,
137                                  "RRD Error: specifying time relative to the 'start' ",
138                                  "or 'end' makes no sense here",
139                                  (char *) NULL);
140                 getopt_cleanup(argc, argv2);
141                 return TCL_ERROR;
142             }
143             last_up = mktime(&last_up_tv.tm) +last_up_tv.offset;
144             if (last_up < 3600 * 24 * 365 * 10) {
145                 Tcl_AppendResult(interp,
146                                  "RRD Error: the first entry to the RRD should be after 1980",
147                                  (char *) NULL);
148                 getopt_cleanup(argc, argv2);
149                 return TCL_ERROR;
150             }
151             getopt_free_element(argv2, argv_i - 1);
152             getopt_free_element(argv2, argv_i);
153         } else if (!strcmp(argv2[argv_i], "--step")
154                    || !strcmp(argv2[argv_i], "-s")) {
155             if (argv_i++ >= argc) {
156                 Tcl_AppendResult(interp, "RRD Error: option '",
157                                  argv2[argv_i - 1], "' needs an argument",
158                                  (char *) NULL);
159                 getopt_cleanup(argc, argv2);
160                 return TCL_ERROR;
161             }
162             long_tmp = atol(argv2[argv_i]);
163             if (long_tmp < 1) {
164                 Tcl_AppendResult(interp,
165                                  "RRD Error: step size should be no less than one second",
166                                  (char *) NULL);
167                 getopt_cleanup(argc, argv2);
168                 return TCL_ERROR;
169             }
170             pdp_step = long_tmp;
171             getopt_free_element(argv2, argv_i - 1);
172             getopt_free_element(argv2, argv_i);
173         } else if (!strcmp(argv2[argv_i], "--")) {
174             getopt_free_element(argv2, argv_i);
175             break;
176         } else if (argv2[argv_i][0] == '-') {
177             Tcl_AppendResult(interp, "RRD Error: unknown option '",
178                              argv2[argv_i], "'", (char *) NULL);
179             getopt_cleanup(argc, argv2);
180             return TCL_ERROR;
181         }
182     }
183
184     getopt_squieeze(&argc, argv2);
185
186     if (argc < 2) {
187         Tcl_AppendResult(interp, "RRD Error: needs rrd filename",
188                          (char *) NULL);
189         getopt_cleanup(argc, argv2);
190         return TCL_ERROR;
191     }
192
193     rrd_create_r(argv2[1], pdp_step, last_up, argc - 2, argv2 + 2);
194
195     getopt_cleanup(argc, argv2);
196
197     if (rrd_test_error()) {
198         Tcl_AppendResult(interp, "RRD Error: ",
199                          rrd_get_error(), (char *) NULL);
200         rrd_clear_error();
201         return TCL_ERROR;
202     }
203
204     return TCL_OK;
205 }
206
207
208
209 /* Thread-safe version */
210 static int Rrd_Dump(
211     ClientData clientData,
212     Tcl_Interp *interp,
213     int argc,
214     CONST84 char *argv[])
215 {
216     if (argc < 2) {
217         Tcl_AppendResult(interp, "RRD Error: needs rrd filename",
218                          (char *) NULL);
219         return TCL_ERROR;
220     }
221
222     rrd_dump_r(argv[1], NULL);
223
224     /* NOTE: rrd_dump() writes to stdout. No interaction with TCL. */
225
226     if (rrd_test_error()) {
227         Tcl_AppendResult(interp, "RRD Error: ",
228                          rrd_get_error(), (char *) NULL);
229         rrd_clear_error();
230         return TCL_ERROR;
231     }
232
233     return TCL_OK;
234 }
235
236
237
238 /* Thread-safe version */
239 static int Rrd_Last(
240     ClientData clientData,
241     Tcl_Interp *interp,
242     int argc,
243     CONST84 char *argv[])
244 {
245     time_t    t;
246
247     if (argc < 2) {
248         Tcl_AppendResult(interp, "RRD Error: needs rrd filename",
249                          (char *) NULL);
250         return TCL_ERROR;
251     }
252
253     t = rrd_last_r(argv[1]);
254
255     if (rrd_test_error()) {
256         Tcl_AppendResult(interp, "RRD Error: ",
257                          rrd_get_error(), (char *) NULL);
258         rrd_clear_error();
259         return TCL_ERROR;
260     }
261
262     Tcl_SetIntObj(Tcl_GetObjResult(interp), t);
263
264     return TCL_OK;
265 }
266
267
268
269 /* Thread-safe version */
270 static int Rrd_Update(
271     ClientData clientData,
272     Tcl_Interp *interp,
273     int argc,
274     CONST84 char *argv[])
275 {
276     int       argv_i;
277     char    **argv2, *template = NULL;
278
279     argv2 = getopt_init(argc, argv);
280
281     for (argv_i = 1; argv_i < argc; argv_i++) {
282         if (!strcmp(argv2[argv_i], "--template")
283             || !strcmp(argv2[argv_i], "-t")) {
284             if (argv_i++ >= argc) {
285                 Tcl_AppendResult(interp, "RRD Error: option '",
286                                  argv2[argv_i - 1], "' needs an argument",
287                                  (char *) NULL);
288                 if (template != NULL) {
289                     free(template);
290                 }
291                 getopt_cleanup(argc, argv2);
292                 return TCL_ERROR;
293             }
294             if (template != NULL) {
295                 free(template);
296             }
297             template = strdup(argv2[argv_i]);
298             getopt_free_element(argv2, argv_i - 1);
299             getopt_free_element(argv2, argv_i);
300         } else if (!strcmp(argv2[argv_i], "--")) {
301             getopt_free_element(argv2, argv_i);
302             break;
303         } else if (argv2[argv_i][0] == '-') {
304             Tcl_AppendResult(interp, "RRD Error: unknown option '",
305                              argv2[argv_i], "'", (char *) NULL);
306             if (template != NULL) {
307                 free(template);
308             }
309             getopt_cleanup(argc, argv2);
310             return TCL_ERROR;
311         }
312     }
313
314     getopt_squieeze(&argc, argv2);
315
316     if (argc < 2) {
317         Tcl_AppendResult(interp, "RRD Error: needs rrd filename",
318                          (char *) NULL);
319         if (template != NULL) {
320             free(template);
321         }
322         getopt_cleanup(argc, argv2);
323         return TCL_ERROR;
324     }
325
326     rrd_update_r(argv2[1], template, argc - 2, argv2 + 2);
327
328     if (template != NULL) {
329         free(template);
330     }
331     getopt_cleanup(argc, argv2);
332
333     if (rrd_test_error()) {
334         Tcl_AppendResult(interp, "RRD Error: ",
335                          rrd_get_error(), (char *) NULL);
336         rrd_clear_error();
337         return TCL_ERROR;
338     }
339
340     return TCL_OK;
341 }
342
343 static int Rrd_Lastupdate(
344     ClientData clientData,
345     Tcl_Interp *interp,
346     int argc,
347     CONST84 char *argv[])
348 {
349     time_t    last_update;
350     char    **argv2;
351     char    **ds_namv;
352     char    **last_ds;
353     char      s[30];
354     Tcl_Obj  *listPtr;
355     unsigned long ds_cnt, i;
356
357     argv2 = getopt_init(argc, argv);
358     if (rrd_lastupdate(argc - 1, argv2, &last_update,
359                        &ds_cnt, &ds_namv, &last_ds) == 0) {
360         listPtr = Tcl_GetObjResult(interp);
361         for (i = 0; i < ds_cnt; i++) {
362             sprintf(s, " %28s", ds_namv[i]);
363             Tcl_ListObjAppendElement(interp, listPtr,
364                                      Tcl_NewStringObj(s, -1));
365             sprintf(s, "\n\n%10lu:", last_update);
366             Tcl_ListObjAppendElement(interp, listPtr,
367                                      Tcl_NewStringObj(s, -1));
368             for (i = 0; i < ds_cnt; i++) {
369                 sprintf(s, " %s", last_ds[i]);
370                 Tcl_ListObjAppendElement(interp, listPtr,
371                                          Tcl_NewStringObj(s, -1));
372                 free(last_ds[i]);
373                 free(ds_namv[i]);
374             }
375             sprintf(s, "\n");
376             Tcl_ListObjAppendElement(interp, listPtr,
377                                      Tcl_NewStringObj(s, -1));
378             free(last_ds);
379             free(ds_namv);
380         }
381     }
382     return TCL_OK;
383 }
384
385 static int Rrd_Fetch(
386     ClientData clientData,
387     Tcl_Interp *interp,
388     int argc,
389     CONST84 char *argv[])
390 {
391     time_t    start, end, j;
392     unsigned long step, ds_cnt, i, ii;
393     rrd_value_t *data, *datai;
394     char    **ds_namv;
395     Tcl_Obj  *listPtr;
396     char      s[30];
397     char    **argv2;
398
399     argv2 = getopt_init(argc, argv);
400     if (rrd_fetch(argc, argv2, &start, &end, &step,
401                   &ds_cnt, &ds_namv, &data) != -1) {
402         datai = data;
403         listPtr = Tcl_GetObjResult(interp);
404         for (j = start; j <= end; j += step) {
405             for (ii = 0; ii < ds_cnt; ii++) {
406                 sprintf(s, "%.2f", *(datai++));
407                 Tcl_ListObjAppendElement(interp, listPtr,
408                                          Tcl_NewStringObj(s, -1));
409             }
410         }
411         for (i = 0; i < ds_cnt; i++)
412             free(ds_namv[i]);
413         free(ds_namv);
414         free(data);
415     }
416     getopt_cleanup(argc, argv2);
417
418     if (rrd_test_error()) {
419         Tcl_AppendResult(interp, "RRD Error: ",
420                          rrd_get_error(), (char *) NULL);
421         rrd_clear_error();
422         return TCL_ERROR;
423     }
424
425     return TCL_OK;
426 }
427
428
429
430 static int Rrd_Graph(
431     ClientData clientData,
432     Tcl_Interp *interp,
433     int argc,
434     CONST84 char *argv[])
435 {
436     Tcl_Channel channel;
437     int       mode, fd2;
438     ClientData fd1;
439     FILE     *stream = NULL;
440     char    **calcpr = NULL;
441     int       rc, xsize, ysize;
442     double    ymin, ymax;
443     char      dimensions[50];
444     char    **argv2;
445     CONST84 char *save;
446
447     /*
448      * If the "filename" is a Tcl fileID, then arrange for rrd_graph() to write to
449      * that file descriptor.  Will this work with windoze?  I have no idea.
450      */
451     if ((channel = Tcl_GetChannel(interp, argv[1], &mode)) != NULL) {
452         /*
453          * It >is< a Tcl fileID
454          */
455         if (!(mode & TCL_WRITABLE)) {
456             Tcl_AppendResult(interp, "channel \"", argv[1],
457                              "\" wasn't opened for writing", (char *) NULL);
458             return TCL_ERROR;
459         }
460         /*
461          * Must flush channel to make sure any buffered data is written before
462          * rrd_graph() writes to the stream
463          */
464         if (Tcl_Flush(channel) != TCL_OK) {
465             Tcl_AppendResult(interp, "flush failed for \"", argv[1], "\": ",
466                              strerror(Tcl_GetErrno()), (char *) NULL);
467             return TCL_ERROR;
468         }
469         if (Tcl_GetChannelHandle(channel, TCL_WRITABLE, &fd1) != TCL_OK) {
470             Tcl_AppendResult(interp,
471                              "cannot get file descriptor associated with \"",
472                              argv[1], "\"", (char *) NULL);
473             return TCL_ERROR;
474         }
475         /*
476          * Must dup() file descriptor so we can fclose(stream), otherwise the fclose()
477          * would close Tcl's file descriptor
478          */
479         if ((fd2 = dup((int) fd1)) == -1) {
480             Tcl_AppendResult(interp,
481                              "dup() failed for file descriptor associated with \"",
482                              argv[1], "\": ", strerror(errno), (char *) NULL);
483             return TCL_ERROR;
484         }
485         /*
486          * rrd_graph() wants a FILE*
487          */
488         if ((stream = fdopen(fd2, "wb")) == NULL) {
489             Tcl_AppendResult(interp,
490                              "fdopen() failed for file descriptor associated with \"",
491                              argv[1], "\": ", strerror(errno), (char *) NULL);
492             close(fd2); /* plug potential file descriptor leak */
493             return TCL_ERROR;
494         }
495
496         save = argv[1];
497         argv[1] = "-";
498         argv2 = getopt_init(argc, argv);
499         argv[1] = save;
500     } else {
501         Tcl_ResetResult(interp);    /* clear error from Tcl_GetChannel() */
502         argv2 = getopt_init(argc, argv);
503     }
504
505     rc = rrd_graph(argc, argv2, &calcpr, &xsize, &ysize, stream, &ymin,
506                    &ymax);
507     getopt_cleanup(argc, argv2);
508
509     if (stream != NULL)
510         fclose(stream); /* plug potential malloc & file descriptor leak */
511
512     if (rc != -1) {
513         sprintf(dimensions, "%d %d", xsize, ysize);
514         Tcl_AppendResult(interp, dimensions, (char *) NULL);
515         if (calcpr) {
516 #if 0
517             int       i;
518
519             for (i = 0; calcpr[i]; i++) {
520                 printf("%s\n", calcpr[i]);
521                 free(calcpr[i]);
522             }
523 #endif
524             free(calcpr);
525         }
526     }
527
528     if (rrd_test_error()) {
529         Tcl_AppendResult(interp, "RRD Error: ",
530                          rrd_get_error(), (char *) NULL);
531         rrd_clear_error();
532         return TCL_ERROR;
533     }
534
535     return TCL_OK;
536 }
537
538
539
540 static int Rrd_Tune(
541     ClientData clientData,
542     Tcl_Interp *interp,
543     int argc,
544     CONST84 char *argv[])
545 {
546     char    **argv2;
547
548     argv2 = getopt_init(argc, argv);
549     rrd_tune(argc, argv2);
550     getopt_cleanup(argc, argv2);
551
552     if (rrd_test_error()) {
553         Tcl_AppendResult(interp, "RRD Error: ",
554                          rrd_get_error(), (char *) NULL);
555         rrd_clear_error();
556         return TCL_ERROR;
557     }
558
559     return TCL_OK;
560 }
561
562
563
564 static int Rrd_Resize(
565     ClientData clientData,
566     Tcl_Interp *interp,
567     int argc,
568     CONST84 char *argv[])
569 {
570     char    **argv2;
571
572     argv2 = getopt_init(argc, argv);
573     rrd_resize(argc, argv2);
574     getopt_cleanup(argc, argv2);
575
576     if (rrd_test_error()) {
577         Tcl_AppendResult(interp, "RRD Error: ",
578                          rrd_get_error(), (char *) NULL);
579         rrd_clear_error();
580         return TCL_ERROR;
581     }
582
583     return TCL_OK;
584 }
585
586
587
588 static int Rrd_Restore(
589     ClientData clientData,
590     Tcl_Interp *interp,
591     int argc,
592     CONST84 char *argv[])
593 {
594     char    **argv2;
595
596     argv2 = getopt_init(argc, argv);
597     rrd_restore(argc, argv2);
598     getopt_cleanup(argc, argv2);
599
600     if (rrd_test_error()) {
601         Tcl_AppendResult(interp, "RRD Error: ",
602                          rrd_get_error(), (char *) NULL);
603         rrd_clear_error();
604         return TCL_ERROR;
605     }
606
607     return TCL_OK;
608 }
609
610
611
612 /*
613  * The following structure defines the commands in the Rrd extension.
614  */
615
616 typedef struct {
617     char     *name;     /* Name of the command. */
618     Tcl_CmdProc *proc;  /* Procedure for command. */
619     int       hide;     /* Hide if safe interpreter */
620 } CmdInfo;
621
622 static CmdInfo rrdCmds[] = {
623     {"Rrd::create", Rrd_Create, 1}, /* Thread-safe version */
624     {"Rrd::dump", Rrd_Dump, 0}, /* Thread-safe version */
625     {"Rrd::last", Rrd_Last, 0}, /* Thread-safe version */
626     {"Rrd::lastupdate", Rrd_Lastupdate, 0}, /* Thread-safe version */
627     {"Rrd::update", Rrd_Update, 1}, /* Thread-safe version */
628     {"Rrd::fetch", Rrd_Fetch, 0},
629     {"Rrd::graph", Rrd_Graph, 1},   /* Due to RRD's API, a safe
630                                        interpreter cannot create
631                                        a graph since it writes to
632                                        a filename supplied by the
633                                        caller */
634     {"Rrd::tune", Rrd_Tune, 1},
635     {"Rrd::resize", Rrd_Resize, 1},
636     {"Rrd::restore", Rrd_Restore, 1},
637     {(char *) NULL, (Tcl_CmdProc *) NULL, 0}
638 };
639
640
641
642 static int init(
643     Tcl_Interp *interp,
644     int safe)
645 {
646     CmdInfo  *cmdInfoPtr;
647     Tcl_CmdInfo info;
648
649     if (Tcl_InitStubs(interp, TCL_VERSION, 0) == NULL)
650         return TCL_ERROR;
651
652     if (Tcl_PkgRequire(interp, "Tcl", TCL_VERSION, 1) == NULL) {
653         return TCL_ERROR;
654     }
655
656     /*
657      * Why a global array?  In keeping with the Rrd:: namespace, why
658      * not simply create a normal variable Rrd::version and set it?
659      */
660     Tcl_SetVar2(interp, "rrd", "version", VERSION, TCL_GLOBAL_ONLY);
661
662     for (cmdInfoPtr = rrdCmds; cmdInfoPtr->name != NULL; cmdInfoPtr++) {
663         /*
664          * Check if the command already exists and return an error
665          * to ensure we detect name clashes while loading the Rrd
666          * extension.
667          */
668         if (Tcl_GetCommandInfo(interp, cmdInfoPtr->name, &info)) {
669             Tcl_AppendResult(interp, "command \"", cmdInfoPtr->name,
670                              "\" already exists", (char *) NULL);
671             return TCL_ERROR;
672         }
673         if (safe && cmdInfoPtr->hide) {
674 #if 0
675             /*
676              * Turns out the one cannot hide a command in a namespace
677              * due to a limitation of Tcl, one can only hide global
678              * commands.  Thus, if we created the commands without
679              * the Rrd:: namespace in a safe interpreter, then the
680              * "unsafe" commands could be hidden -- which would allow
681              * an owning interpreter either un-hiding them or doing
682              * an "interp invokehidden".  If the Rrd:: namespace is
683              * used, then it's still possible for the owning interpreter
684              * to fake out the missing commands:
685              *
686              *   # Make all Rrd::* commands available in master interperter
687              *   package require Rrd
688              *   set safe [interp create -safe]
689              *   # Make safe Rrd::* commands available in safe interperter
690              *   interp invokehidden $safe -global load ./tclrrd1.2.11.so
691              *   # Provide the safe interpreter with the missing commands
692              *   $safe alias Rrd::update do_update $safe
693              *   proc do_update {which_interp $args} {
694              *     # Do some checking maybe...
695              *       :
696              *     return [eval Rrd::update $args]
697              *   }
698              *
699              * Our solution for now is to just not create the "unsafe"
700              * commands in a safe interpreter.
701              */
702             if (Tcl_HideCommand(interp, cmdInfoPtr->name, cmdInfoPtr->name) !=
703                 TCL_OK)
704                 return TCL_ERROR;
705 #endif
706         } else
707             Tcl_CreateCommand(interp, cmdInfoPtr->name, cmdInfoPtr->proc,
708                               (ClientData) NULL, (Tcl_CmdDeleteProc *) NULL);
709     }
710
711     if (Tcl_PkgProvide(interp, "Rrd", VERSION) != TCL_OK) {
712         return TCL_ERROR;
713     }
714
715     return TCL_OK;
716 }
717
718 int Tclrrd_Init(
719     Tcl_Interp *interp)
720 {
721     return init(interp, 0);
722 }
723
724 /*
725  * See the comments above and note how few commands are considered "safe"...
726  * Using rrdtool in a safe interpreter has very limited functionality.  It's
727  * tempting to just return TCL_ERROR and forget about it.
728  */
729 int Tclrrd_SafeInit(
730     Tcl_Interp *interp)
731 {
732     return init(interp, 1);
733 }