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