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 */
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"
25 /* support pre-8.4 tcl */
27 #ifndef CONST84
28 # define CONST84
29 #endif
31 extern int Tclrrd_Init(
32 Tcl_Interp *interp);
33 extern int Tclrrd_SafeInit(
34 Tcl_Interp *interp);
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;
50 argv2 = calloc(argc, sizeof(char *));
51 for (i = 0; i < argc; i++) {
52 argv2[i] = strdup(argv[i]);
53 }
54 return argv2;
55 }
57 static void getopt_cleanup(
58 int argc,
59 char **argv2)
60 {
61 int i;
63 for (i = 0; i < argc; i++) {
64 if (argv2[i] != NULL) {
65 free(argv2[i]);
66 }
67 }
68 free(argv2);
69 }
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 }
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;
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 }
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;
117 argv2 = getopt_init(argc, argv);
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 }
184 getopt_squieeze(&argc, argv2);
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 }
193 rrd_create_r(argv2[1], pdp_step, last_up, argc - 2, argv2 + 2);
195 getopt_cleanup(argc, argv2);
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 }
204 return TCL_OK;
205 }
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 }
222 rrd_dump_r(argv[1], NULL);
224 /* NOTE: rrd_dump() writes to stdout. No interaction with TCL. */
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 }
233 return TCL_OK;
234 }
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;
247 if (argc < 2) {
248 Tcl_AppendResult(interp, "RRD Error: needs rrd filename",
249 (char *) NULL);
250 return TCL_ERROR;
251 }
253 t = rrd_last_r(argv[1]);
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 }
262 Tcl_SetIntObj(Tcl_GetObjResult(interp), t);
264 return TCL_OK;
265 }
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;
279 argv2 = getopt_init(argc, argv);
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 }
314 getopt_squieeze(&argc, argv2);
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 }
326 rrd_update_r(argv2[1], template, argc - 2, argv2 + 2);
328 if (template != NULL) {
329 free(template);
330 }
331 getopt_cleanup(argc, argv2);
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 }
340 return TCL_OK;
341 }
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;
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 }
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;
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);
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 }
425 return TCL_OK;
426 }
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;
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 }
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 }
505 rc = rrd_graph(argc, argv2, &calcpr, &xsize, &ysize, stream, &ymin,
506 &ymax);
507 getopt_cleanup(argc, argv2);
509 if (stream != NULL)
510 fclose(stream); /* plug potential malloc & file descriptor leak */
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;
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 }
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 }
535 return TCL_OK;
536 }
540 static int Rrd_Tune(
541 ClientData clientData,
542 Tcl_Interp *interp,
543 int argc,
544 CONST84 char *argv[])
545 {
546 char **argv2;
548 argv2 = getopt_init(argc, argv);
549 rrd_tune(argc, argv2);
550 getopt_cleanup(argc, argv2);
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 }
559 return TCL_OK;
560 }
564 static int Rrd_Resize(
565 ClientData clientData,
566 Tcl_Interp *interp,
567 int argc,
568 CONST84 char *argv[])
569 {
570 char **argv2;
572 argv2 = getopt_init(argc, argv);
573 rrd_resize(argc, argv2);
574 getopt_cleanup(argc, argv2);
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 }
583 return TCL_OK;
584 }
588 static int Rrd_Restore(
589 ClientData clientData,
590 Tcl_Interp *interp,
591 int argc,
592 CONST84 char *argv[])
593 {
594 char **argv2;
596 argv2 = getopt_init(argc, argv);
597 rrd_restore(argc, argv2);
598 getopt_cleanup(argc, argv2);
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 }
607 return TCL_OK;
608 }
612 /*
613 * The following structure defines the commands in the Rrd extension.
614 */
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;
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 };
642 static int init(
643 Tcl_Interp *interp,
644 int safe)
645 {
646 CmdInfo *cmdInfoPtr;
647 Tcl_CmdInfo info;
649 if (Tcl_InitStubs(interp, TCL_VERSION, 0) == NULL)
650 return TCL_ERROR;
652 if (Tcl_PkgRequire(interp, "Tcl", TCL_VERSION, 1) == NULL) {
653 return TCL_ERROR;
654 }
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);
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 }
711 if (Tcl_PkgProvide(interp, "Rrd", VERSION) != TCL_OK) {
712 return TCL_ERROR;
713 }
715 return TCL_OK;
716 }
718 int Tclrrd_Init(
719 Tcl_Interp *interp)
720 {
721 return init(interp, 0);
722 }
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 }