source: subversion/applications/utils/mod_tile/mod_tile.c @ 12950

Last change on this file since 12950 was 12950, checked in by twain, 11 years ago

mod_tile: New apache directives and options for multiple tile sets

File size: 20.5 KB
Line 
1#include "apr.h"
2#include "apr_strings.h"
3#include "apr_thread_proc.h"    /* for RLIMIT stuff */
4#include "apr_optional.h"
5#include "apr_buckets.h"
6#include "apr_lib.h"
7#include "apr_poll.h"
8
9#define APR_WANT_STRFUNC
10#define APR_WANT_MEMFUNC
11#include "apr_want.h"
12
13#include "util_filter.h"
14#include "ap_config.h"
15#include "httpd.h"
16#include "http_config.h"
17#include "http_request.h"
18#include "http_core.h"
19#include "http_protocol.h"
20#include "http_main.h"
21#include "http_log.h"
22#include "util_script.h"
23#include "ap_mpm.h"
24#include "mod_core.h"
25#include "mod_cgi.h"
26#include "util_md5.h"
27
28module AP_MODULE_DECLARE_DATA tile_module;
29
30#include <stdio.h>
31#include <stdlib.h>
32#include <string.h>
33#include <stdarg.h>
34#include <sys/types.h>
35#include <sys/socket.h>
36#include <sys/un.h>
37#include <unistd.h>
38#include <fcntl.h>
39#include <errno.h>
40#include <limits.h>
41#include <time.h>
42
43#include "gen_tile.h"
44#include "protocol.h"
45#include "render_config.h"
46#include "store.h"
47#include "dir_utils.h"
48
49#define INILINE_MAX 256
50typedef struct {
51    char xmlname[XMLCONFIG_MAX];
52    char baseuri[PATH_MAX];
53} tile_config_rec;
54
55typedef struct {
56    apr_array_header_t *configs;
57} tile_server_conf;
58
59enum tileState { tileMissing, tileOld, tileCurrent };
60
61static int error_message(request_rec *r, const char *format, ...)
62                 __attribute__ ((format (printf, 2, 3)));
63
64static int error_message(request_rec *r, const char *format, ...)
65{
66    va_list ap;
67    va_start(ap, format);
68    int len;
69    char *msg;
70
71    len = vasprintf(&msg, format, ap);
72
73    if (msg) {
74        //ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "%s", msg);
75        ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "%s", msg);
76        r->content_type = "text/plain";
77        if (!r->header_only)
78            ap_rputs(msg, r);
79        free(msg);
80    }
81
82    return OK;
83}
84
85
86int socket_init(request_rec *r)
87{
88    const char *spath = RENDER_SOCKET;
89    int fd;
90    struct sockaddr_un addr;
91
92    //fprintf(stderr, "Starting rendering client\n");
93
94    fd = socket(PF_UNIX, SOCK_STREAM, 0);
95    if (fd < 0) {
96        ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "failed to create unix socket");
97        return FD_INVALID;
98    }
99
100    bzero(&addr, sizeof(addr));
101    addr.sun_family = AF_UNIX;
102    strncpy(addr.sun_path, spath, sizeof(addr.sun_path));
103
104    if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
105        ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r, "socket connect failed for: %s", spath);
106        close(fd);
107        return FD_INVALID;
108    }
109    return fd;
110}
111
112static pthread_key_t key;
113static pthread_once_t key_once = PTHREAD_ONCE_INIT;
114
115static void pfd_free(void *ptr)
116{
117    int *pfd = ptr;
118
119    if (*pfd != FD_INVALID)
120        close(*pfd);
121    free(pfd);
122}
123
124static void make_key(void)
125{
126    (void) pthread_key_create(&key, pfd_free);
127}
128
129int request_tile(request_rec *r, struct protocol *cmd, int dirtyOnly)
130{
131    int *pfd;
132    int ret = 0;
133    int retry = 1;
134    struct protocol resp;
135
136    (void) pthread_once(&key_once, make_key);
137    if ((pfd = pthread_getspecific(key)) == NULL) {
138        pfd = malloc(sizeof(*pfd));
139        if (!pfd)
140            return 0;
141        *pfd = FD_INVALID;
142        (void) pthread_setspecific(key, pfd);
143    }
144
145    if (*pfd == FD_INVALID) {
146        *pfd = socket_init(r);
147
148        if (*pfd == FD_INVALID) {
149            //fprintf(stderr, "Failed to connect to renderer\n");
150            ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "Failed to connected to renderer");
151            return 0;
152        } else {
153            ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "Connected to renderer");
154        }
155    }
156
157    // cmd has already been partial filled, fill in the rest
158    cmd->ver = PROTO_VER;
159    cmd->cmd = dirtyOnly ? cmdDirty : cmdRender;
160
161    ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "Requesting xml(%s) z(%d) x(%d) y(%d)", cmd->xmlname, cmd->z, cmd->x, cmd->y);
162    do {
163        ret = send(*pfd, cmd, sizeof(struct protocol), 0);
164
165        if (ret == sizeof(struct protocol))
166            break;
167
168        if (errno != EPIPE)
169            return 0;
170
171         close(*pfd);
172         *pfd = socket_init(r);
173         if (*pfd == FD_INVALID)
174             return 0;
175         ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "Reconnected to renderer");
176    } while (retry--);
177
178    if (!dirtyOnly) {
179        struct timeval tv = { REQUEST_TIMEOUT, 0 };
180        fd_set rx;
181        int s;
182
183        while (1) {
184            FD_ZERO(&rx);
185            FD_SET(*pfd, &rx);
186            s = select((*pfd)+1, &rx, NULL, NULL, &tv);
187            if (s == 1) {
188                bzero(&resp, sizeof(struct protocol));
189                ret = recv(*pfd, &resp, sizeof(struct protocol), 0);
190                if (ret != sizeof(struct protocol)) {
191                    if (errno == EPIPE) {
192                        close(*pfd);
193                        *pfd = FD_INVALID;
194                    }
195                    //perror("recv error");
196                    break;
197                }
198 
199                if (cmd->x == resp.x && cmd->y == resp.y && cmd->z == resp.z && !strcmp(cmd->xmlname, resp.xmlname)) {
200                    if (resp.cmd == cmdDone)
201                        return 1;
202                    else
203                        return 0;
204                } else {
205                    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
206                       "Response does not match request: xml(%s,%s) z(%d,%d) x(%d,%d) y(%d,%d)", cmd->xmlname, 
207                       resp.xmlname, cmd->z, resp.z, cmd->x, resp.x, cmd->y, resp.y);                   
208                }
209            } else if (s == 0) {
210                break;
211            } else {
212                if (errno == EPIPE) {
213                    close(*pfd);
214                    *pfd = FD_INVALID;
215                    break;
216                }
217            }
218        }
219    }
220    return 0;
221}
222
223static apr_time_t getPlanetTime(request_rec *r)
224{
225    static apr_time_t last_check;
226    static apr_time_t planet_timestamp;
227    static pthread_mutex_t planet_lock = PTHREAD_MUTEX_INITIALIZER;
228    apr_time_t now = r->request_time;
229    struct apr_finfo_t s;
230
231    pthread_mutex_lock(&planet_lock);
232    // Only check for updates periodically
233    if (now < last_check + apr_time_from_sec(300)) {
234        pthread_mutex_unlock(&planet_lock);
235        return planet_timestamp;
236    }
237
238    last_check = now;
239    if (apr_stat(&s, PLANET_TIMESTAMP, APR_FINFO_MIN, r->pool) != APR_SUCCESS) {
240        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Planet timestamp file " PLANET_TIMESTAMP " is missing");
241        // Make something up
242        planet_timestamp = now - apr_time_from_sec(3 * 24 * 60 * 60);
243    } else {
244        if (s.mtime != planet_timestamp) {
245            ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "Planet file updated");
246            planet_timestamp = s.mtime;
247        }
248    }
249    pthread_mutex_unlock(&planet_lock);
250    return planet_timestamp;
251}
252
253static enum tileState tile_state_once(request_rec *r)
254{
255    apr_status_t rv;
256    apr_finfo_t *finfo = &r->finfo;
257
258    if (!(finfo->valid & APR_FINFO_MTIME)) {
259        rv = apr_stat(finfo, r->filename, APR_FINFO_MIN, r->pool);
260        if (rv != APR_SUCCESS)
261            return tileMissing;
262    }
263
264    if (finfo->mtime < getPlanetTime(r))
265        return tileOld;
266
267    return tileCurrent;
268}
269
270static enum tileState tile_state(request_rec *r, struct protocol *cmd)
271{
272    enum tileState state = tile_state_once(r);
273#ifdef METATILEFALLBACK
274    if (state == tileMissing) {
275
276        // Try fallback to plain PNG
277        char path[PATH_MAX];
278        xyz_to_path(path, sizeof(path), cmd->xmlname, cmd->x, cmd->y, cmd->z);
279        r->filename = apr_pstrdup(r->pool, path);
280        state = tile_state_once(r);
281        ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "png fallback %d/%d/%d",x,y,z);
282
283        if (state == tileMissing) {
284            // PNG not available either, if it gets rendered, it'll now be a .meta
285            xyz_to_meta(path, sizeof(path), cmd->xmlname, cmd->x, cmd->y, cmd->z);
286            r->filename = apr_pstrdup(r->pool, path);
287        }
288    }
289#endif
290    return state;
291}
292
293static void add_expiry(request_rec *r, struct protocol * cmd)
294{
295    apr_time_t expires, holdoff, planetTimestamp;
296    apr_table_t *t = r->headers_out;
297    enum tileState state = tile_state(r, cmd);
298    char *timestr;
299
300    /* Append expiry headers ... */
301
302    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "expires(%s), uri(%s), filename(%s), path_info(%s)\n",
303                  r->handler, r->uri, r->filename, r->path_info);
304
305    // We estimate an expiry based on when the next planet dump is (or was) due
306    // If we are past this time already then round up to request time
307    // Then add a randomisation of up to 3 hours
308    planetTimestamp = (state == tileCurrent) ? (getPlanetTime(r) + apr_time_from_sec(PLANET_INTERVAL)) : getPlanetTime(r);
309    holdoff = apr_time_from_sec(3 * 60 * 60) * (rand() / (RAND_MAX + 1.0));
310    expires = MAX(r->request_time, planetTimestamp) + holdoff;
311
312    apr_table_mergen(t, "Cache-Control",
313                     apr_psprintf(r->pool, "max-age=%" APR_TIME_T_FMT,
314                     apr_time_sec(expires - r->request_time)));
315    timestr = apr_palloc(r->pool, APR_RFC822_DATE_LEN);
316    apr_rfc822_date(timestr, expires);
317    apr_table_setn(t, "Expires", timestr);
318}
319
320
321double get_load_avg(request_rec *r)
322{
323    double loadavg[1];
324    int n = getloadavg(loadavg, 1);
325
326    if (n < 1)
327        return 1000;
328    else
329        return loadavg[0];
330}
331
332static int tile_handler_dirty(request_rec *r)
333{
334    if(strcmp(r->handler, "tile_dirty"))
335        return DECLINED;
336
337    struct protocol * cmd = (struct protocol *)ap_get_module_config(r->request_config, &tile_module);
338    if (cmd == NULL)
339        return DECLINED;
340
341    request_tile(r, cmd, 1);
342    return error_message(r, "Tile submitted for rendering\n");
343}
344
345static int tile_storage_hook(request_rec *r)
346{
347//    char abs_path[PATH_MAX];
348    int avg;
349    enum tileState state;
350
351    ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "tile_storage_hook: handler(%s), uri(%s), filename(%s), path_info(%s)",
352                  r->handler, r->uri, r->filename, r->path_info);
353
354    if (!r->handler)
355        return DECLINED;
356
357    // Any status request is OK
358    if (!strcmp(r->handler, "tile_status"))
359        return OK;
360
361    if (strcmp(r->handler, "tile_serve") && strcmp(r->handler, "tile_dirty"))
362        return DECLINED;
363
364    struct protocol * cmd = (struct protocol *)ap_get_module_config(r->request_config, &tile_module);
365    if (cmd == NULL)
366        return DECLINED;
367/*
368should already be done
369    // Generate the tile filename
370#ifdef METATILE
371    xyz_to_meta(abs_path, sizeof(abs_path), cmd->xmlname, cmd->x, cmd->y, cmd->z);
372#else
373    xyz_to_path(abs_path, sizeof(abs_path), cmd->xmlname, cmd->x, cmd->y, cmd->z);
374#endif
375    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "abs_path(%s)", abs_path);
376    r->filename = apr_pstrdup(r->pool, abs_path);
377*/
378    avg = get_load_avg(r);
379    state = tile_state(r, cmd);
380
381    switch (state) {
382        case tileCurrent:
383            return OK;
384            break;
385        case tileOld:
386            if (avg > MAX_LOAD_OLD) {
387               // Too much load to render it now, mark dirty but return old tile
388               request_tile(r, cmd, 1);
389               return OK;
390            }
391            break;
392        case tileMissing:
393            if (avg > MAX_LOAD_MISSING) {
394               request_tile(r, cmd, 1);
395               return HTTP_NOT_FOUND;
396            }
397            break;
398    }
399
400    if (request_tile(r, cmd, 0)) {
401        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Update file info abs_path(%s)", r->filename);
402        // Need to update fileinfo for new rendered tile
403        apr_stat(&r->finfo, r->filename, APR_FINFO_MIN, r->pool);
404        return OK;
405    }
406
407    if (state == tileOld)
408        return OK;
409
410    return HTTP_NOT_FOUND;
411}
412
413static int tile_handler_status(request_rec *r)
414{
415    enum tileState state;
416    char time_str[APR_CTIME_LEN];
417
418    if(strcmp(r->handler, "tile_status"))
419        return DECLINED;
420
421    struct protocol * cmd = (struct protocol *)ap_get_module_config(r->request_config, &tile_module);
422    if (cmd == NULL){
423        sleep(CLIENT_PENALTY);
424        return HTTP_NOT_FOUND;
425    }
426
427    state = tile_state(r, cmd);
428    if (state == tileMissing)
429        return error_message(r, "Unable to find a tile at %s\n", r->filename);
430    apr_ctime(time_str, r->finfo.mtime);
431
432    return error_message(r, "Tile is %s. Last rendered at %s\n", (state == tileOld) ? "due to be rendered" : "clean", time_str);
433}
434
435static int tile_handler_serve(request_rec *r)
436{
437    const int tile_max = MAX_SIZE;
438    unsigned char *buf;
439    int len;
440    apr_status_t errstatus;
441
442    if(strcmp(r->handler, "tile_serve"))
443        return DECLINED;
444
445    struct protocol * cmd = (struct protocol *)ap_get_module_config(r->request_config, &tile_module);
446    if (cmd == NULL){
447        sleep(CLIENT_PENALTY);
448        return HTTP_NOT_FOUND;
449    }
450
451    ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "tile_handler_serve: xml(%s) z(%d) x(%d) y(%d)", cmd->xmlname, cmd->z, cmd->x, cmd->y);
452
453    // FIXME: It is a waste to do the malloc + read if we are fulfilling a HEAD or returning a 304.
454    buf = malloc(tile_max);
455    if (!buf)
456        return HTTP_INTERNAL_SERVER_ERROR;
457
458    len = tile_read(cmd->xmlname, cmd->x, cmd->y, cmd->z, buf, tile_max);
459    if (len > 0) {
460#if 0
461        // Set default Last-Modified and Etag headers
462        ap_update_mtime(r, r->finfo.mtime);
463        ap_set_last_modified(r);
464        ap_set_etag(r);
465#else
466        // Use MD5 hash as only cache attribute.
467        // If a tile is re-rendered and produces the same output
468        // then we can continue to use the previous cached copy
469        char *md5 = ap_md5_binary(r->pool, buf, len);
470        apr_table_setn(r->headers_out, "ETag",
471                        apr_psprintf(r->pool, "\"%s\"", md5));
472#endif
473        ap_set_content_type(r, "image/png");
474        ap_set_content_length(r, len);
475        add_expiry(r, cmd);
476        if ((errstatus = ap_meets_conditions(r)) != OK) {
477            free(buf);
478            return errstatus;
479        } else {
480            ap_rwrite(buf, len, r);
481            free(buf);
482            return OK;
483        }
484    }
485    free(buf);
486    //ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "len = %d", len);
487
488    return DECLINED;
489}
490
491static int tile_translate(request_rec *r)
492{
493    ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "tile_translate: uri(%s)", r->uri);
494
495    int i,n,limit,oob;
496    char option[11];
497
498    ap_conf_vector_t *sconf = r->server->module_config;
499    tile_server_conf *scfg = ap_get_module_config(sconf, &tile_module);
500
501    tile_config_rec *tile_configs = (tile_config_rec *) scfg->configs->elts;
502    for (i = 0; i < scfg->configs->nelts; ++i) {
503        tile_config_rec *tile_config = &tile_configs[i];
504   
505        ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "tile_translate: baseuri(%s) name(%s)", tile_config->baseuri, tile_config->xmlname);
506
507        if (!strncmp(tile_config->baseuri, r->uri, strlen(tile_config->baseuri))) {
508
509            struct protocol * cmd = (struct protocol *) apr_pcalloc(r->pool, sizeof(struct protocol));
510            bzero(cmd, sizeof(struct protocol));
511
512            n = sscanf(r->uri+strlen(tile_config->baseuri), "%d/%d/%d.png/%10s", &(cmd->z), &(cmd->x), &(cmd->y), option);
513            if (n < 3) return DECLINED;
514
515            oob = (cmd->z < 0 || cmd->z > MAX_ZOOM);
516            if (!oob) {
517                 // valid x/y for tiles are 0 ... 2^zoom-1
518                 limit = (1 << cmd->z) - 1;
519                 oob =  (cmd->x < 0 || cmd->x > limit || cmd->y < 0 || cmd->y > limit);
520            }
521
522            if (oob) {
523                sleep(CLIENT_PENALTY);
524                return HTTP_NOT_FOUND;
525            }
526
527            strcpy(cmd->xmlname, tile_config->xmlname);
528
529            // Store a copy for later
530            ap_set_module_config(r->request_config, &tile_module, cmd);
531
532            // Generate the tile filename?
533            char abs_path[PATH_MAX];
534#ifdef METATILE
535            xyz_to_meta(abs_path, sizeof(abs_path), cmd->xmlname, cmd->x, cmd->y, cmd->z);
536#else
537            xyz_to_path(abs_path, sizeof(abs_path), cmd->xmlname, cmd->x, cmd->y, cmd->z);
538#endif
539            r->filename = apr_pstrdup(r->pool, abs_path);
540
541            if (n == 4) {
542                if (!strcmp(option, "status")) r->handler = "tile_status";
543                else if (!strcmp(option, "dirty")) r->handler = "tile_dirty";
544                else return DECLINED;
545            } else {
546                r->handler = "tile_serve";
547            }
548
549            ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "tile_translate: op(%s) xml(%s) z(%d) x(%d) y(%d)", r->handler , cmd->xmlname, cmd->z, cmd->x, cmd->y);
550
551            return OK;
552        }
553    }   
554    return DECLINED;
555}
556
557static void register_hooks(__attribute__((unused)) apr_pool_t *p)
558{
559    ap_hook_handler(tile_handler_serve, NULL, NULL, APR_HOOK_MIDDLE);
560    ap_hook_handler(tile_handler_dirty, NULL, NULL, APR_HOOK_MIDDLE);
561    ap_hook_handler(tile_handler_status, NULL, NULL, APR_HOOK_MIDDLE);
562    ap_hook_translate_name(tile_translate, NULL, NULL, APR_HOOK_MIDDLE);
563    ap_hook_map_to_storage(tile_storage_hook, NULL, NULL, APR_HOOK_FIRST);
564}
565
566static const char *add_tile_config(cmd_parms *cmd, void *mconfig, const char *baseuri, const char *name)
567{
568    if (strlen(name) == 0) {
569        return "ConfigName value must not be null";
570    }
571
572    tile_server_conf *scfg = ap_get_module_config(cmd->server->module_config, &tile_module);
573    tile_config_rec *tilecfg = apr_array_push(scfg->configs);   
574
575    strncpy(tilecfg->baseuri, baseuri, PATH_MAX-1);
576    tilecfg->baseuri[PATH_MAX-1] = 0;
577    strncpy(tilecfg->xmlname, name, XMLCONFIG_MAX-1);
578    tilecfg->xmlname[XMLCONFIG_MAX-1] = 0;
579   
580    return NULL;
581}
582
583static const char *load_tile_config(cmd_parms *cmd, void *mconfig, const char *conffile)
584{
585    FILE * hini ;
586    char filename[PATH_MAX];
587    char xmlname[XMLCONFIG_MAX];
588    char line[INILINE_MAX];
589    char key[INILINE_MAX];
590    char value[INILINE_MAX];
591    const char * result;
592
593    if (strlen(conffile) == 0) {
594        strcpy(filename, RENDERD_CONFIG);
595    } else {
596        strcpy(filename, conffile);
597    }
598
599    // Load the config
600    if ((hini=fopen(filename, "r"))==NULL) {
601        return "Unable to open config file";
602    }
603
604    while (fgets(line, INILINE_MAX, hini)!=NULL) {
605        if (line[0] == '#') continue;
606        if (line[strlen(line)-1] == '\n') line[strlen(line)-1] = 0;
607        if (line[0] == '[') {
608            if (strlen(line) >= XMLCONFIG_MAX){
609                return "XML name too long";
610            }
611            sscanf(line, "[%[^]]", xmlname);
612        } else if (sscanf(line, "%[^=]=%[^;#]", key, value) == 2
613               ||  sscanf(line, "%[^=]=\"%[^\"]\"", key, value) == 2) {
614
615            if (!strcmp(key, "URI")){
616                if (strlen(value) >= PATH_MAX){
617                    return "URI too long";
618                }
619                result = add_tile_config(cmd, mconfig, value, xmlname);
620                if (result != NULL) return result;
621            }
622        }
623    }
624    fclose(hini);
625    return NULL;
626}
627
628static void *create_tile_config(apr_pool_t *p, server_rec *s)
629{
630    tile_server_conf * scfg = (tile_server_conf *) apr_pcalloc(p, sizeof(tile_server_conf));
631    scfg->configs = apr_array_make(p, 4, sizeof(tile_config_rec));
632    return scfg;
633}
634
635static void *merge_tile_config(apr_pool_t *p, void *basev, void *overridesv)
636{
637    tile_server_conf * scfg = (tile_server_conf *) apr_pcalloc(p, sizeof(tile_server_conf));
638    tile_server_conf * scfg_base = (tile_server_conf *) basev;
639    tile_server_conf * scfg_over = (tile_server_conf *) overridesv;
640
641    scfg->configs = apr_array_append(p, scfg_base->configs, scfg_over->configs);
642    return scfg;
643}
644
645static const command_rec tile_cmds[] =
646{
647    AP_INIT_TAKE1(
648        "LoadTileConfigFile",            /* directive name */
649        load_tile_config,                /* config action routine */
650        NULL,                            /* argument to include in call */
651        OR_OPTIONS,                      /* where available */
652        "load an entire renderd config file"  /* directive description */
653    ),
654    AP_INIT_TAKE2(
655        "AddTileConfig",                 /* directive name */
656        add_tile_config,                 /* config action routine */
657        NULL,                            /* argument to include in call */
658        OR_OPTIONS,                      /* where available */
659        "path and name of renderd config to use"  /* directive description */
660    ),
661    {NULL}
662};
663
664module AP_MODULE_DECLARE_DATA tile_module =
665{
666    STANDARD20_MODULE_STUFF,
667    NULL,                                /* dir config creater */
668    NULL,                                /* dir merger --- default is to override */
669    create_tile_config,                  /* server config */
670    merge_tile_config,                   /* merge server config */
671    tile_cmds,                           /* command apr_table_t */
672    register_hooks                       /* register hooks */
673};
674
Note: See TracBrowser for help on using the repository browser.