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