/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * htcacheclean.c: simple program for cleaning of
 * the disk cache of the Apache HTTP server
 *
 * Contributed by Andreas Steinmetz <ast domdv.de>
 * 8 Oct 2004
 */

#include "apr.h"
#include "apr_lib.h"
#include "apr_strings.h"
#include "apr_file_io.h"
#include "apr_file_info.h"
#include "apr_pools.h"
#include "apr_hash.h"
#include "apr_thread_proc.h"
#include "apr_signal.h"
#include "apr_getopt.h"
#include "apr_md5.h"
#include "apr_ring.h"
#include "apr_date.h"
#include "apr_buckets.h"

#include "../modules/cache/cache_common.h"
#include "../modules/cache/cache_disk_common.h"

#if APR_HAVE_UNISTD_H
#include <unistd.h>
#endif
#if APR_HAVE_STDLIB_H
#include <stdlib.h>
#endif

/* define the following for debugging */
#undef DEBUG

/*
 * Note: on Linux delays <= 2ms are busy waits without
 *       scheduling, so never use a delay <= 2ms below
 */

#define NICE_DELAY    10000     /* usecs */
#define DELETE_NICE   10        /* be nice after this amount of delete ops */
#define STAT_ATTEMPTS 10        /* maximum stat attempts for a file */
#define STAT_DELAY    5000      /* usecs */
#define HEADER        1         /* headers file */
#define DATA          2         /* body file */
#define TEMP          4         /* temporary file */
#define HEADERDATA    (HEADER|DATA)
#define MAXDEVIATION  3600      /* secs */
#define SECS_PER_MIN  60
#define KBYTE         1024
#define MBYTE         1048576
#define GBYTE         1073741824

#define DIRINFO (APR_FINFO_MTIME|APR_FINFO_SIZE|APR_FINFO_TYPE|APR_FINFO_LINK)

typedef struct _direntry {
    APR_RING_ENTRY(_direntry) link;
    int type;         /* type of file/fileset: TEMP, HEADER, DATA, HEADERDATA */
    apr_time_t htime; /* headers file modification time */
    apr_time_t dtime; /* body file modification time */
    apr_off_t hsize;  /* headers file size */
    apr_off_t dsize;  /* body or temporary file size */
    char *basename;   /* file/fileset base name */
} DIRENTRY;

typedef struct _entry {
    APR_RING_ENTRY(_entry) link;
    apr_time_t expire;        /* cache entry exiration time */
    apr_time_t response_time; /* cache entry time of last response to client */
    apr_time_t htime;         /* headers file modification time */
    apr_time_t dtime;         /* body file modification time */
    apr_off_t hsize;          /* headers file size */
    apr_off_t dsize;          /* body or temporary file size */
    char *basename;           /* fileset base name */
} ENTRY;


static int delcount;    /* file deletion count for nice mode */
static int interrupted; /* flag: true if SIGINT or SIGTERM occurred */
static int realclean;   /* flag: true means user said apache is not running */
static int verbose;     /* flag: true means print statistics */
static int benice;      /* flag: true means nice mode is activated */
static int dryrun;      /* flag: true means dry run, don't actually delete
                                 anything */
static int deldirs;     /* flag: true means directories should be deleted */
static int listurls;    /* flag: true means list cached urls */
static int listextended;/* flag: true means list cached urls */
static int baselen;     /* string length of the path to the proxy directory */
static apr_time_t now;  /* start time of this processing run */

static apr_file_t *errfile;   /* stderr file handle */
static apr_file_t *outfile;   /* stdout file handle */
static apr_off_t unsolicited; /* file size summary for deleted unsolicited
                                 files */
static APR_RING_ENTRY(_entry) root; /* ENTRY ring anchor */

/* short program name as called */
static const char *shortname = "htcacheclean";

/* what did we clean? */
struct stats {
    apr_off_t total;
    apr_off_t sum;
    apr_off_t max;
    apr_off_t ntotal;
    apr_off_t nodes;
    apr_off_t inodes;
    apr_off_t etotal;
    apr_off_t entries;
    apr_off_t dfuture;
    apr_off_t dexpired;
    apr_off_t dfresh;
};


#ifdef DEBUG
/*
 * fake delete for debug purposes
 */
#define apr_file_remove fake_file_remove
static void fake_file_remove(char *pathname, apr_pool_t *p)
{
    apr_finfo_t info;

    /* stat and printing to simulate some deletion system load and to
       display what would actually have happened */
    apr_stat(&info, pathname, DIRINFO, p);
    apr_file_printf(errfile, "would delete %s" APR_EOL_STR, pathname);
}
#endif

/*
 * called on SIGINT or SIGTERM
 */
static void setterm(int unused)
{
#ifdef DEBUG
    apr_file_printf(errfile, "interrupt" APR_EOL_STR);
#endif
    interrupted = 1;
}

/*
 * called in out of memory condition
 */
static int oom(int unused)
{
    static int called = 0;

    /* be careful to call exit() only once */
    if (!called) {
        called = 1;
        exit(1);
    }
    return APR_ENOMEM;
}

/*
 * print purge statistics
 */
static void printstats(char *path, struct stats *s)
{
    char ttype, stype, mtype, utype;
    apr_off_t tfrag, sfrag, ufrag;

    if (!verbose) {
        return;
    }

    ttype = 'K';
    tfrag = ((s->total * 10) / KBYTE) % 10;
    s->total /= KBYTE;
    if (s->total >= KBYTE) {
        ttype = 'M';
        tfrag = ((s->total * 10) / KBYTE) % 10;
        s->total /= KBYTE;
    }

    stype = 'K';
    sfrag = ((s->sum * 10) / KBYTE) % 10;
    s->sum /= KBYTE;
    if (s->sum >= KBYTE) {
        stype = 'M';
        sfrag = ((s->sum * 10) / KBYTE) % 10;
        s->sum /= KBYTE;
    }

    mtype = 'K';
    s->max /= KBYTE;
    if (s->max >= KBYTE) {
        mtype = 'M';
        s->max /= KBYTE;
    }

    apr_file_printf(errfile, "Cleaned %s. Statistics:" APR_EOL_STR, path);
    if (unsolicited) {
        utype = 'K';
        ufrag = ((unsolicited * 10) / KBYTE) % 10;
        unsolicited /= KBYTE;
        if (unsolicited >= KBYTE) {
            utype = 'M';
            ufrag = ((unsolicited * 10) / KBYTE) % 10;
            unsolicited /= KBYTE;
        }
        if (!unsolicited && !ufrag) {
            ufrag = 1;
        }
        apr_file_printf(errfile, "unsolicited size %d.%d%c" APR_EOL_STR,
                        (int)(unsolicited), (int)(ufrag), utype);
    }
    apr_file_printf(errfile, "size limit %" APR_OFF_T_FMT ".0%c" APR_EOL_STR,
            s->max, mtype);
    apr_file_printf(errfile, "inodes limit %" APR_OFF_T_FMT APR_EOL_STR,
            s->inodes);
    apr_file_printf(
            errfile,
            "total size was %" APR_OFF_T_FMT ".%" APR_OFF_T_FMT "%c, total size now "
            "%" APR_OFF_T_FMT ".%" APR_OFF_T_FMT "%c" APR_EOL_STR, s->total,
            tfrag, ttype, s->sum, sfrag, stype);
    apr_file_printf(errfile, "total inodes was %" APR_OFF_T_FMT
            ", total %sinodes now "
            "%" APR_OFF_T_FMT APR_EOL_STR, s->ntotal, dryrun && deldirs ? "estimated "
            : "", s->nodes);
    apr_file_printf(
            errfile,
            "total entries was %" APR_OFF_T_FMT ", total entries now %" APR_OFF_T_FMT
            APR_EOL_STR, s->etotal, s->entries);
    apr_file_printf(
            errfile,
            "%" APR_OFF_T_FMT " entries deleted (%" APR_OFF_T_FMT " from future, %"
            APR_OFF_T_FMT " expired, %" APR_OFF_T_FMT " fresh)" APR_EOL_STR,
            (s->etotal - s->entries), s->dfuture, s->dexpired, s->dfresh);
}

/**
 * Round the value up to the given threshold.
 */
static apr_size_t round_up(apr_size_t val, apr_off_t round) {
    if (round > 1) {
        return (apr_size_t)(((val + round - 1) / round) * round);
    }
    return val;
}

/*
 * delete parent directories
 */
static void delete_parent(const char *path, const char *basename,
        apr_off_t *nodes, apr_pool_t *pool)
{
    char *nextpath, *name;
    apr_pool_t *p;

    /* temp pool, otherwise lots of memory could be allocated */
    apr_pool_create(&p, pool);
    name = apr_pstrdup(p, basename);

    /* If asked to delete dirs, do so now. We don't care if it fails.
     * If it fails, it likely means there was something else there.
     */
    if (deldirs && !dryrun) {
        const char *vary;
        char *end = strrchr(name, '/');
        while (end) {
            *end = 0;

            /* remove the directory */
            nextpath = apr_pstrcat(p, path, "/", name, NULL);
            if (!apr_dir_remove(nextpath, p)) {
                (*nodes)--;

                /* vary directory found? */
                vary = strstr(name, CACHE_VDIR_SUFFIX);
                if (vary && !vary[sizeof(CACHE_VDIR_SUFFIX) - 1]) {
                    nextpath = apr_pstrcat(p, path, "/", apr_pstrndup(p, name, vary
                            - name), NULL);
                    if (!apr_file_remove(nextpath, p)) {
                        (*nodes)--;
                    }
                }

            }
            else {
                break;
            }
            end = strrchr(name, '/');
        }
    }

    apr_pool_destroy(p);

    if (benice) {
        if (++delcount >= DELETE_NICE) {
            apr_sleep(NICE_DELAY);
            delcount = 0;
        }
    }

}

/*
 * delete a single file
 */
static void delete_file(char *path, char *basename, apr_off_t *nodes,
        apr_pool_t *pool)
{
    char *nextpath;
    apr_pool_t *p;

    /* temp pool, otherwise lots of memory could be allocated */
    apr_pool_create(&p, pool);
    nextpath = apr_pstrcat(p, path, "/", basename, NULL);

    if (dryrun) {
        apr_finfo_t finfo;
        if (!apr_stat(&finfo, nextpath, APR_FINFO_NLINK, p)) {
            (*nodes)--;
        }
    }
    else if (!apr_file_remove(nextpath, p)) {
        (*nodes)--;
    }

    apr_pool_destroy(p);

    if (benice) {
        if (++delcount >= DELETE_NICE) {
            apr_sleep(NICE_DELAY);
            delcount = 0;
        }
    }

    delete_parent(path, basename, nodes, pool);

}

/*
 * delete cache file set
 */
static void delete_entry(char *path, char *basename, apr_off_t *nodes,
        apr_pool_t *pool)
{
    char *nextpath;
    apr_pool_t *p;

    /* temp pool, otherwise lots of memory could be allocated */
    apr_pool_create(&p, pool);

    nextpath = apr_pstrcat(p, path, "/", basename, CACHE_HEADER_SUFFIX, NULL);
    if (dryrun) {
        apr_finfo_t finfo;
        if (!apr_stat(&finfo, nextpath, APR_FINFO_NLINK, p)) {
            (*nodes)--;
        }
    }
    else if (!apr_file_remove(nextpath, p)) {
        (*nodes)--;
    }

    nextpath = apr_pstrcat(p, path, "/", basename, CACHE_DATA_SUFFIX, NULL);
    if (dryrun) {
        apr_finfo_t finfo;
        if (!apr_stat(&finfo, nextpath, APR_FINFO_NLINK, p)) {
            (*nodes)--;
        }
    }
    else if (!apr_file_remove(nextpath, p)) {
        (*nodes)--;
    }

    apr_pool_destroy(p);

    if (benice) {
        delcount += 2;
        if (delcount >= DELETE_NICE) {
            apr_sleep(NICE_DELAY);
            delcount = 0;
        }
    }

    delete_parent(path, basename, nodes, pool);

}

/*
 * list the cache directory tree
 */
static int list_urls(char *path, apr_pool_t *pool, apr_off_t round)
{
    apr_dir_t *dir;
    apr_finfo_t info;
    apr_size_t len;
    apr_pool_t *p;
    apr_file_t *fd;
    const char *ext, *nextpath;
    char *url;
    apr_uint32_t format;
    disk_cache_info_t disk_info;

    apr_pool_create(&p, pool);

    if (apr_dir_open(&dir, path, p) != APR_SUCCESS) {
        return 1;
    }

    while (apr_dir_read(&info, APR_FINFO_TYPE, dir) == APR_SUCCESS && !interrupted) {

        if (info.filetype == APR_DIR) {
            if (!strcmp(info.name, ".") || !strcmp(info.name, "..")) {
                continue;
            }

            if (list_urls(apr_pstrcat(p, path, "/", info.name, NULL), pool, round)) {
                return 1;
            }
        }

        else if (info.filetype == APR_REG) {

            ext = strchr(info.name, '.');

            if (ext && !strcasecmp(ext, CACHE_HEADER_SUFFIX)) {

                nextpath = apr_pstrcat(p, path, "/", info.name, NULL);

                if (apr_file_open(&fd, nextpath, APR_FOPEN_READ
                        | APR_FOPEN_BINARY, APR_OS_DEFAULT, p) == APR_SUCCESS) {
                    len = sizeof(format);
                    if (apr_file_read_full(fd, &format, len, &len)
                            == APR_SUCCESS) {
                        if (format == DISK_FORMAT_VERSION) {
                            apr_off_t offset = 0;

                            apr_file_seek(fd, APR_SET, &offset);

                            len = sizeof(disk_cache_info_t);

                            if (apr_file_read_full(fd, &disk_info, len, &len)
                                    == APR_SUCCESS) {
                                len = disk_info.name_len;
                                url = apr_palloc(p, len + 1);
                                url[len] = 0;

                                if (apr_file_read_full(fd, url, len, &len)
                                        == APR_SUCCESS) {

                                    if (listextended) {
                                        apr_finfo_t hinfo, dinfo;

                                        /* stat the header file */
                                        if (APR_SUCCESS != apr_file_info_get(
                                                &hinfo, APR_FINFO_SIZE, fd)) {
                                            /* ignore the file */
                                        }
                                        else if (disk_info.has_body && APR_SUCCESS
                                                != apr_stat(
                                                        &dinfo,
                                                        apr_pstrcat(
                                                                p,
                                                                path,
                                                                "/",
                                                                apr_pstrndup(
                                                                        p,
                                                                        info.name,
                                                                        ext
                                                                                - info.name),
                                                                CACHE_DATA_SUFFIX,
                                                                NULL),
                                                        APR_FINFO_SIZE
                                                                | APR_FINFO_IDENT,
                                                        p)) {
                                            /* ignore the file */
                                        }
                                        else if (disk_info.has_body && (dinfo.device
                                                != disk_info.device
                                                || dinfo.inode
                                                        != disk_info.inode)) {
                                            /* ignore the file */
                                        }
                                        else {

                                            apr_file_printf(
                                                    outfile,
                                                    "%s %" APR_SIZE_T_FMT
                                                    " %" APR_SIZE_T_FMT
                                                    " %d %" APR_SIZE_T_FMT
                                                    " %" APR_TIME_T_FMT
                                                    " %" APR_TIME_T_FMT
                                                    " %" APR_TIME_T_FMT
                                                    " %" APR_TIME_T_FMT
                                                    " %d %d\n",
                                                    url,
                                                    round_up((apr_size_t)hinfo.size, round),
                                                    round_up(
                                                            disk_info.has_body ? (apr_size_t)dinfo.size
                                                                    : 0, round),
                                                    disk_info.status,
                                                    disk_info.entity_version,
                                                    disk_info.date,
                                                    disk_info.expire,
                                                    disk_info.request_time,
                                                    disk_info.response_time,
                                                    disk_info.has_body,
                                                    disk_info.header_only);
                                        }
                                    }
                                    else {
                                        apr_finfo_t dinfo;

                                        /* stat the data file */
                                        if (disk_info.has_body && APR_SUCCESS
                                                != apr_stat(
                                                        &dinfo,
                                                        apr_pstrcat(
                                                                p,
                                                                path,
                                                                "/",
                                                                apr_pstrndup(
                                                                        p,
                                                                        info.name,
                                                                        ext
                                                                                - info.name),
                                                                CACHE_DATA_SUFFIX,
                                                                NULL),
                                                        APR_FINFO_SIZE
                                                                | APR_FINFO_IDENT,
                                                        p)) {
                                            /* ignore the file */
                                        }
                                        else if (disk_info.has_body && (dinfo.device
                                                != disk_info.device
                                                || dinfo.inode
                                                        != disk_info.inode)) {
                                            /* ignore the file */
                                        }
                                        else {
                                            apr_file_printf(outfile, "%s\n",
                                                    url);
                                        }
                                    }
                                }

                                break;
                            }
                        }
                    }
                    apr_file_close(fd);

                }
            }
        }

    }

    apr_dir_close(dir);

    if (interrupted) {
        return 1;
    }

    apr_pool_destroy(p);

    if (benice) {
        apr_sleep(NICE_DELAY);
    }

    if (interrupted) {
        return 1;
    }

    return 0;
}

/*
 * walk the cache directory tree
 */
static int process_dir(char *path, apr_pool_t *pool, apr_off_t *nodes)
{
    apr_dir_t *dir;
    apr_pool_t *p;
    apr_hash_t *h;
    apr_hash_index_t *i;
    apr_file_t *fd;
    apr_status_t status;
    apr_finfo_t info;
    apr_size_t len;
    apr_time_t current, deviation;
    char *nextpath, *base, *ext, *dirpath;
    APR_RING_ENTRY(_direntry) anchor;
    DIRENTRY *d, *t, *n;
    ENTRY *e;
    int skip, retries;
    disk_cache_info_t disk_info;

    APR_RING_INIT(&anchor, _direntry, link);
    apr_pool_create(&p, pool);
    h = apr_hash_make(p);
    fd = NULL;
    deviation = MAXDEVIATION * APR_USEC_PER_SEC;

    if (apr_dir_open(&dir, path, p) != APR_SUCCESS) {
        return 1;
    }

    while (apr_dir_read(&info, 0, dir) == APR_SUCCESS && !interrupted) {
        if (!strcmp(info.name, ".") || !strcmp(info.name, "..")) {
            continue;
        }
        d = apr_pcalloc(p, sizeof(DIRENTRY));
        d->basename = apr_pstrcat(p, path, "/", info.name, NULL);
        APR_RING_INSERT_TAIL(&anchor, d, _direntry, link);
        (*nodes)++;
    }

    apr_dir_close(dir);

    if (interrupted) {
        return 1;
    }

    skip = baselen + 1;

    for (d = APR_RING_FIRST(&anchor);
         !interrupted && d != APR_RING_SENTINEL(&anchor, _direntry, link);
         d=n) {
        n = APR_RING_NEXT(d, link);
        base = strrchr(d->basename, '/');
        if (!base++) {
            base = d->basename;
        }
        ext = strchr(base, '.');

        /* there may be temporary files which may be gone before
         * processing, always skip these if not in realclean mode
         */
        if (!ext && !realclean) {
            if (!strncasecmp(base, AP_TEMPFILE_BASE, AP_TEMPFILE_BASELEN)
                && strlen(base) == AP_TEMPFILE_NAMELEN) {
                continue;
            }
        }

        /* this may look strange but apr_stat() may return an error which
         * is system dependent and there may be transient failures,
         * so just blindly retry for a short while
         */
        retries = STAT_ATTEMPTS;
        status = APR_SUCCESS;
        do {
            if (status != APR_SUCCESS) {
                apr_sleep(STAT_DELAY);
            }
            status = apr_stat(&info, d->basename, DIRINFO, p);
        } while (status != APR_SUCCESS && !interrupted && --retries);

        /* what may happen here is that apache did create a file which
         * we did detect but then does delete the file before we can
         * get file information, so if we don't get any file information
         * we will ignore the file in this case
         */
        if (status != APR_SUCCESS) {
            if (!realclean && !interrupted) {
                continue;
            }
            return 1;
        }

        if (info.filetype == APR_DIR) {
            dirpath = apr_pstrdup(p, d->basename);
            if (process_dir(d->basename, pool, nodes)) {
                return 1;
            }
            /* When given the -t option htcacheclean does not
             * delete directories that are already empty, so we'll do that here
             * since process_dir checks all the directories.
             * If it fails, it likely means there was something else there.
             */
            if (deldirs && !dryrun){
                apr_dir_remove(dirpath, p);
            }
            continue;
        }

        if (info.filetype != APR_REG) {
            continue;
        }

        if (!ext) {
            if (!strncasecmp(base, AP_TEMPFILE_BASE, AP_TEMPFILE_BASELEN)
                && strlen(base) == AP_TEMPFILE_NAMELEN) {
                d->basename += skip;
                d->type = TEMP;
                d->dsize = info.size;
                apr_hash_set(h, d->basename, APR_HASH_KEY_STRING, d);
            }
            continue;
        }

        if (!strcasecmp(ext, CACHE_HEADER_SUFFIX)) {
            *ext = '\0';
            d->basename += skip;
            /* if a user manually creates a '.header' file */
            if (d->basename[0] == '\0') {
                continue;
            }
            t = apr_hash_get(h, d->basename, APR_HASH_KEY_STRING);
            if (t) {
                d = t;
            }
            d->type |= HEADER;
            d->htime = info.mtime;
            d->hsize = info.size;
            apr_hash_set(h, d->basename, APR_HASH_KEY_STRING, d);
            continue;
        }

        if (!strcasecmp(ext, CACHE_DATA_SUFFIX)) {
            *ext = '\0';
            d->basename += skip;
            /* if a user manually creates a '.data' file */
            if (d->basename[0] == '\0') {
                continue;
            }
            t = apr_hash_get(h, d->basename, APR_HASH_KEY_STRING);
            if (t) {
                d = t;
            }
            d->type |= DATA;
            d->dtime = info.mtime;
            d->dsize = info.size;
            apr_hash_set(h, d->basename, APR_HASH_KEY_STRING, d);
        }
    }

    if (interrupted) {
        return 1;
    }

    path[baselen] = '\0';

    for (i = apr_hash_first(p, h); i && !interrupted; i = apr_hash_next(i)) {
        void *hvalue;
        apr_uint32_t format;

        apr_hash_this(i, NULL, NULL, &hvalue);
        d = hvalue;

        switch(d->type) {
        case HEADERDATA:
            nextpath = apr_pstrcat(p, path, "/", d->basename,
                                   CACHE_HEADER_SUFFIX, NULL);
            if (apr_file_open(&fd, nextpath, APR_FOPEN_READ | APR_FOPEN_BINARY,
                              APR_OS_DEFAULT, p) == APR_SUCCESS) {
                len = sizeof(format);
                if (apr_file_read_full(fd, &format, len,
                                       &len) == APR_SUCCESS) {
                    if (format == DISK_FORMAT_VERSION) {
                        apr_off_t offset = 0;

                        apr_file_seek(fd, APR_SET, &offset);

                        len = sizeof(disk_cache_info_t);

                        if (apr_file_read_full(fd, &disk_info, len,
                                               &len) == APR_SUCCESS) {
                            apr_file_close(fd);
                            e = apr_palloc(pool, sizeof(ENTRY));
                            APR_RING_INSERT_TAIL(&root, e, _entry, link);
                            e->expire = disk_info.expire;
                            e->response_time = disk_info.response_time;
                            e->htime = d->htime;
                            e->dtime = d->dtime;
                            e->hsize = d->hsize;
                            e->dsize = d->dsize;
                            e->basename = apr_pstrdup(pool, d->basename);
                            if (!disk_info.has_body) {
                                delete_file(path, apr_pstrcat(p, path, "/",
                                        d->basename, CACHE_DATA_SUFFIX, NULL),
                                        nodes, p);
                            }
                            break;
                        }
                        else {
                            apr_file_close(fd);
                        }
                    }
                    else if (format == VARY_FORMAT_VERSION) {
                        apr_finfo_t finfo;

                        /* This must be a URL that added Vary headers later,
                         * so kill the orphaned .data file
                         */
                        apr_file_close(fd);

                        if (apr_stat(&finfo, apr_pstrcat(p, nextpath,
                                CACHE_VDIR_SUFFIX, NULL), APR_FINFO_TYPE, p)
                                || finfo.filetype != APR_DIR) {
                            delete_entry(path, d->basename, nodes, p);
                        }
                        else {
                            delete_file(path, apr_pstrcat(p, path, "/",
                                    d->basename, CACHE_DATA_SUFFIX, NULL),
                                    nodes, p);
                        }
                        break;
                    }
                    else {
                        /* We didn't recognise the format, kill the files */
                        apr_file_close(fd);
                        delete_entry(path, d->basename, nodes, p);
                        break;
                    }
                }
                else {
                    apr_file_close(fd);
                }

            }
            /* we have a somehow unreadable headers file which is associated
             * with a data file. this may be caused by apache currently
             * rewriting the headers file. thus we may delete the file set
             * either in realclean mode or if the headers file modification
             * timestamp is not within a specified positive or negative offset
             * to the current time.
             */
            current = apr_time_now();
            if (realclean || d->htime < current - deviation
                || d->htime > current + deviation) {
                delete_entry(path, d->basename, nodes, p);
                unsolicited += d->hsize;
                unsolicited += d->dsize;
            }
            break;

        /* single data and header files may be deleted either in realclean
         * mode or if their modification timestamp is not within a
         * specified positive or negative offset to the current time.
         * this handling is necessary due to possible race conditions
         * between apache and this process
         */
        case HEADER:
            current = apr_time_now();
            nextpath = apr_pstrcat(p, path, "/", d->basename,
                                   CACHE_HEADER_SUFFIX, NULL);
            if (apr_file_open(&fd, nextpath, APR_FOPEN_READ | APR_FOPEN_BINARY,
                              APR_OS_DEFAULT, p) == APR_SUCCESS) {
                len = sizeof(format);
                if (apr_file_read_full(fd, &format, len,
                                       &len) == APR_SUCCESS) {
                    if (format == VARY_FORMAT_VERSION) {
                        apr_time_t expires;

                        len = sizeof(expires);

                        if (apr_file_read_full(fd, &expires, len,
                                               &len) == APR_SUCCESS) {
                            apr_finfo_t finfo;

                            apr_file_close(fd);

                            if (apr_stat(&finfo, apr_pstrcat(p, nextpath,
                                    CACHE_VDIR_SUFFIX, NULL), APR_FINFO_TYPE, p)
                                    || finfo.filetype != APR_DIR) {
                                delete_entry(path, d->basename, nodes, p);
                            }
                            else if (expires < current) {
                                delete_entry(path, d->basename, nodes, p);
                            }

                            break;
                        }
                    }
                    else if (format == DISK_FORMAT_VERSION) {
                        apr_off_t offset = 0;

                        apr_file_seek(fd, APR_SET, &offset);

                        len = sizeof(disk_cache_info_t);

                        if (apr_file_read_full(fd, &disk_info, len,
                                               &len) == APR_SUCCESS) {
                            apr_file_close(fd);
                            e = apr_palloc(pool, sizeof(ENTRY));
                            APR_RING_INSERT_TAIL(&root, e, _entry, link);
                            e->expire = disk_info.expire;
                            e->response_time = disk_info.response_time;
                            e->htime = d->htime;
                            e->dtime = d->dtime;
                            e->hsize = d->hsize;
                            e->dsize = d->dsize;
                            e->basename = apr_pstrdup(pool, d->basename);
                            break;
                        }
                        else {
                            apr_file_close(fd);
                        }
                    }
                    else {
                        apr_file_close(fd);
                        delete_entry(path, d->basename, nodes, p);
                        break;
                    }
                }
                else {
                    apr_file_close(fd);
                }
            }

            if (realclean || d->htime < current - deviation
                || d->htime > current + deviation) {
                delete_entry(path, d->basename, nodes, p);
                unsolicited += d->hsize;
            }
            break;

        case DATA:
            current = apr_time_now();
            if (realclean || d->dtime < current - deviation
                || d->dtime > current + deviation) {
                delete_entry(path, d->basename, nodes, p);
                unsolicited += d->dsize;
            }
            break;

        /* temp files may only be deleted in realclean mode which
         * is asserted above if a tempfile is in the hash array
         */
        case TEMP:
            delete_file(path, d->basename, nodes, p);
            unsolicited += d->dsize;
            break;
        }
    }

    if (interrupted) {
        return 1;
    }

    apr_pool_destroy(p);

    if (benice) {
        apr_sleep(NICE_DELAY);
    }

    if (interrupted) {
        return 1;
    }

    return 0;
}

/*
 * purge cache entries
 */
static void purge(char *path, apr_pool_t *pool, apr_off_t max,
        apr_off_t inodes, apr_off_t nodes, apr_off_t round)
{
    ENTRY *e, *n, *oldest;

    struct stats s;
    s.sum = 0;
    s.entries = 0;
    s.dfuture = 0;
    s.dexpired = 0;
    s.dfresh = 0;
    s.max = max;
    s.nodes = nodes;
    s.inodes = inodes;
    s.ntotal = nodes;

    for (e = APR_RING_FIRST(&root);
         e != APR_RING_SENTINEL(&root, _entry, link);
         e = APR_RING_NEXT(e, link)) {
        s.sum += round_up((apr_size_t)e->hsize, round);
        s.sum += round_up((apr_size_t)e->dsize, round);
        s.entries++;
    }

    s.total = s.sum;
    s.etotal = s.entries;

    if ((!s.max || s.sum <= s.max) && (!s.inodes || s.nodes <= s.inodes)) {
        printstats(path, &s);
        return;
    }

    /* process all entries with a timestamp in the future, this may
     * happen if a wrong system time is corrected
     */

    for (e = APR_RING_FIRST(&root);
         e != APR_RING_SENTINEL(&root, _entry, link) && !interrupted;) {
        n = APR_RING_NEXT(e, link);
        if (e->response_time > now || e->htime > now || e->dtime > now) {
            delete_entry(path, e->basename, &s.nodes, pool);
            s.sum -= round_up((apr_size_t)e->hsize, round);
            s.sum -= round_up((apr_size_t)e->dsize, round);
            s.entries--;
            s.dfuture++;
            APR_RING_REMOVE(e, link);
            if ((!s.max || s.sum <= s.max) && (!s.inodes || s.nodes <= s.inodes)) {
                if (!interrupted) {
                    printstats(path, &s);
                }
                return;
            }
        }
        e = n;
    }

    if (interrupted) {
        return;
    }

    /* process all entries with are expired */
    for (e = APR_RING_FIRST(&root);
         e != APR_RING_SENTINEL(&root, _entry, link) && !interrupted;) {
        n = APR_RING_NEXT(e, link);
        if (e->expire != APR_DATE_BAD && e->expire < now) {
            delete_entry(path, e->basename, &s.nodes, pool);
            s.sum -= round_up((apr_size_t)e->hsize, round);
            s.sum -= round_up((apr_size_t)e->dsize, round);
            s.entries--;
            s.dexpired++;
            APR_RING_REMOVE(e, link);
            if ((!s.max || s.sum <= s.max) && (!s.inodes || s.nodes <= s.inodes)) {
                if (!interrupted) {
                    printstats(path, &s);
                }
                return;
            }
        }
        e = n;
    }

    if (interrupted) {
         return;
    }

    /* process remaining entries oldest to newest, the check for an empty
     * ring actually isn't necessary except when the compiler does
     * corrupt 64bit arithmetics which happened to me once, so better safe
     * than sorry
     */
    while (!((!s.max || s.sum <= s.max) && (!s.inodes || s.nodes <= s.inodes))
            && !interrupted && !APR_RING_EMPTY(&root, _entry, link)) {
        oldest = APR_RING_FIRST(&root);

        for (e = APR_RING_NEXT(oldest, link);
             e != APR_RING_SENTINEL(&root, _entry, link);
             e = APR_RING_NEXT(e, link)) {
            if (e->dtime < oldest->dtime) {
                oldest = e;
            }
        }

        delete_entry(path, oldest->basename, &s.nodes, pool);
        s.sum -= round_up((apr_size_t)oldest->hsize, round);
        s.sum -= round_up((apr_size_t)oldest->dsize, round);
        s.entries--;
        s.dfresh++;
        APR_RING_REMOVE(oldest, link);
    }

    if (!interrupted) {
        printstats(path, &s);
    }
}

static apr_status_t remove_directory(apr_pool_t *pool, const char *dir)
{
    apr_status_t rv;
    apr_dir_t *dirp;
    apr_finfo_t dirent;

    rv = apr_dir_open(&dirp, dir, pool);
    if (APR_STATUS_IS_ENOENT(rv)) {
        return rv;
    }
    if (rv != APR_SUCCESS) {
        apr_file_printf(errfile, "Could not open directory %s: %pm" APR_EOL_STR,
                dir, &rv);
        return rv;
    }

    while (apr_dir_read(&dirent, APR_FINFO_DIRENT | APR_FINFO_TYPE, dirp)
            == APR_SUCCESS) {
        if (dirent.filetype == APR_DIR) {
            if (strcmp(dirent.name, ".") && strcmp(dirent.name, "..")) {
                rv = remove_directory(pool, apr_pstrcat(pool, dir, "/",
                        dirent.name, NULL));
                /* tolerate the directory not being empty, the cache may have
                 * attempted to recreate the directory in the mean time.
                 */
                if (APR_SUCCESS != rv && APR_ENOTEMPTY != rv) {
                    break;
                }
            }
        } else {
            const char *file = apr_pstrcat(pool, dir, "/", dirent.name, NULL);
            rv = apr_file_remove(file, pool);
            if (APR_SUCCESS != rv) {
                apr_file_printf(errfile,
                        "Could not remove file '%s': %pm" APR_EOL_STR, file,
                        &rv);
                break;
            }
        }
    }

    apr_dir_close(dirp);

    if (rv == APR_SUCCESS) {
        rv = apr_dir_remove(dir, pool);
        if (APR_ENOTEMPTY == rv) {
            rv = APR_SUCCESS;
        }
        if (rv != APR_SUCCESS) {
            apr_file_printf(errfile, "Could not remove directory %s: %pm" APR_EOL_STR,
                    dir, &rv);
        }
    }

    return rv;
}

static apr_status_t find_directory(apr_pool_t *pool, const char *base,
        const char *rest)
{
    apr_status_t rv;
    apr_dir_t *dirp;
    apr_finfo_t dirent;
    int found = 0, files = 0;
    const char *header = apr_pstrcat(pool, rest, CACHE_HEADER_SUFFIX, NULL);
    const char *data = apr_pstrcat(pool, rest, CACHE_DATA_SUFFIX, NULL);
    const char *vdir = apr_pstrcat(pool, rest, CACHE_HEADER_SUFFIX,
            CACHE_VDIR_SUFFIX, NULL);
    const char *dirname = NULL;

    rv = apr_dir_open(&dirp, base, pool);
    if (rv != APR_SUCCESS) {
        apr_file_printf(errfile, "Could not open directory %s: %pm" APR_EOL_STR,
                base, &rv);
        return rv;
    }

    rv = APR_ENOENT;

    while (apr_dir_read(&dirent, APR_FINFO_DIRENT | APR_FINFO_TYPE, dirp)
            == APR_SUCCESS) {
        int len = strlen(dirent.name);
        int restlen = strlen(rest);
        if (dirent.filetype == APR_DIR && !strncmp(rest, dirent.name, len)) {
            dirname = apr_pstrcat(pool, base, "/", dirent.name, NULL);
            rv = find_directory(pool, dirname, rest + (len < restlen ? len
                    : restlen));
            if (APR_SUCCESS == rv) {
                found = 1;
            }
        }
        if (dirent.filetype == APR_DIR) {
            if (!strcmp(dirent.name, vdir)) {
                files = 1;
            }
        }
        if (dirent.filetype == APR_REG) {
            if (!strcmp(dirent.name, header) || !strcmp(dirent.name, data)) {
                files = 1;
            }
        }
    }

    apr_dir_close(dirp);

    if (files) {
        rv = APR_SUCCESS;
        if (!dryrun) {
            const char *remove;
            apr_status_t status;

            remove = apr_pstrcat(pool, base, "/", header, NULL);
            status = apr_file_remove(remove, pool);
            if (status != APR_SUCCESS && !APR_STATUS_IS_ENOENT(status)) {
                apr_file_printf(errfile, "Could not remove file %s: %pm" APR_EOL_STR,
                        remove, &status);
                rv = status;
            }

            remove = apr_pstrcat(pool, base, "/", data, NULL);
            status = apr_file_remove(remove, pool);
            if (status != APR_SUCCESS && !APR_STATUS_IS_ENOENT(status)) {
                apr_file_printf(errfile, "Could not remove file %s: %pm" APR_EOL_STR,
                        remove, &status);
                rv = status;
            }

            status = remove_directory(pool, apr_pstrcat(pool, base, "/", vdir, NULL));
            if (status != APR_SUCCESS && !APR_STATUS_IS_ENOENT(status)) {
                rv = status;
            }
        }
    }

    /* If asked to delete dirs, do so now. We don't care if it fails.
     * If it fails, it likely means there was something else there.
     */
    if (dirname && deldirs && !dryrun) {
        apr_dir_remove(dirname, pool);
    }

    if (found) {
        return APR_SUCCESS;
    }

    return rv;
}

/**
 * Delete a specific URL from the cache.
 */
static apr_status_t delete_url(apr_pool_t *pool, const char *proxypath, const char *url)
{
    apr_md5_ctx_t context;
    unsigned char digest[16];
    char tmp[23];
    int i, k;
    unsigned int x;
    static const char enc_table[64] =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_@";

    apr_md5_init(&context);
    apr_md5_update(&context, (const unsigned char *) url, strlen(url));
    apr_md5_final(digest, &context);

    /* encode 128 bits as 22 characters, using a modified uuencoding
     * the encoding is 3 bytes -> 4 characters* i.e. 128 bits is
     * 5 x 3 bytes + 1 byte -> 5 * 4 characters + 2 characters
     */
    for (i = 0, k = 0; i < 15; i += 3) {
        x = (digest[i] << 16) | (digest[i + 1] << 8) | digest[i + 2];
        tmp[k++] = enc_table[x >> 18];
        tmp[k++] = enc_table[(x >> 12) & 0x3f];
        tmp[k++] = enc_table[(x >> 6) & 0x3f];
        tmp[k++] = enc_table[x & 0x3f];
    }

    /* one byte left */
    x = digest[15];
    tmp[k++] = enc_table[x >> 2]; /* use up 6 bits */
    tmp[k++] = enc_table[(x << 4) & 0x3f];
    tmp[k] = 0;

    /* automatically find the directory levels */
    return find_directory(pool, proxypath, tmp);
}

/*
 * usage info
 */
#define NL APR_EOL_STR
static void usage(const char *error)
{
    if (error) {
        apr_file_printf(errfile, "%s error: %s\n", shortname, error);
    }
    apr_file_printf(errfile,
    "%s -- program for cleaning the disk cache."                             NL
    "Usage: %s [-Dvtrn] -pPATH [-lLIMIT|-LLIMIT] [-PPIDFILE]"                NL
    "       %s [-nti] -dINTERVAL -pPATH [-lLIMIT|-LLIMIT] [-PPIDFILE]"       NL
    "       %s [-Dvt] -pPATH URL ..."                                        NL
                                                                             NL
    "Options:"                                                               NL
    "  -d   Daemonize and repeat cache cleaning every INTERVAL minutes."     NL
    "       This option is mutually exclusive with the -D, -v and -r"        NL
    "       options."                                                        NL
                                                                             NL
    "  -D   Do a dry run and don't delete anything. This option is mutually" NL
    "       exclusive with the -d option. When doing a dry run and deleting" NL
    "       directories with -t, the inodes reported deleted in the stats"   NL
    "       cannot take into account the directories deleted, and will be"   NL
    "       marked as an estimate."                                          NL
                                                                             NL
    "  -v   Be verbose and print statistics. This option is mutually"        NL
    "       exclusive with the -d option."                                   NL
                                                                             NL
    "  -r   Clean thoroughly. This assumes that the Apache web server is "   NL
    "       not running. This option is mutually exclusive with the -d"      NL
    "       option and implies -t."                                          NL
                                                                             NL
    "  -n   Be nice. This causes slower processing in favour of other"       NL
    "       processes."                                                      NL
                                                                             NL
    "  -t   Delete all empty directories. By default only cache files are"   NL
    "       removed, however with some configurations the large number of"   NL
    "       directories created may require attention."                      NL
                                                                             NL
    "  -p   Specify PATH as the root directory of the disk cache."           NL
                                                                             NL
    "  -P   Specify PIDFILE as the file to write the pid to."                NL
                                                                             NL
    "  -R   Specify amount to round sizes up to."                            NL
                                                                             NL
    "  -l   Specify LIMIT as the total disk cache size limit. Attach 'K'"    NL
    "       or 'M' to the number for specifying KBytes or MBytes."           NL
                                                                             NL
    "  -L   Specify LIMIT as the total disk cache inode limit."              NL
                                                                             NL
    "  -i   Be intelligent and run only when there was a modification of"    NL
    "       the disk cache. This option is only possible together with the"  NL
    "       -d option."                                                      NL
                                                                             NL
    "  -a   List the URLs currently stored in the cache. Variants of the"    NL
    "       same URL will be listed once for each variant."                  NL
                                                                             NL
    "  -A   List the URLs currently stored in the cache, along with their"   NL
    "       attributes in the following order: url, header size, body size," NL
    "       status, entity version, date, expiry, request time,"             NL
    "       response time, body present, head request."                      NL
                                                                             NL
    "Should an URL be provided on the command line, the URL will be"         NL
    "deleted from the cache. A reverse proxied URL is made up as follows:"   NL
    "http://<hostname>:<port><path>?[query]. So, for the path \"/\" on the"  NL
    "host \"localhost\" and port 80, the URL to delete becomes"              NL
    "\"http://localhost:80/?\". Note the '?' in the URL must always be"      NL
    "specified explicitly, whether a query string is present or not."        NL,
    shortname,
    shortname,
    shortname,
    shortname
    );

    exit(1);
}
#undef NL

static void usage_repeated_arg(apr_pool_t *pool, char option) {
    usage(apr_psprintf(pool,
                       "The option '%c' cannot be specified more than once",
                       option));
}

static void log_pid(apr_pool_t *pool, const char *pidfilename, apr_file_t **pidfile)
{
    apr_status_t status;
    pid_t mypid = getpid();

    if (APR_SUCCESS == (status = apr_file_open(pidfile, pidfilename,
                APR_FOPEN_WRITE | APR_FOPEN_CREATE | APR_FOPEN_TRUNCATE |
                APR_FOPEN_DELONCLOSE, APR_FPROT_UREAD | APR_FPROT_UWRITE |
                APR_FPROT_GREAD | APR_FPROT_WREAD, pool))) {
        apr_file_printf(*pidfile, "%" APR_PID_T_FMT APR_EOL_STR, mypid);
    }
    else {
        if (errfile) {
            apr_file_printf(errfile,
                            "Could not write the pid file '%s': %pm" APR_EOL_STR,
                            pidfilename, &status);
        }
        exit(1);
    }
}

/*
 * main
 */
int main(int argc, const char * const argv[])
{
    apr_off_t max, inodes, round;
    apr_time_t current, repeat, delay, previous;
    apr_status_t status;
    apr_pool_t *pool, *instance;
    apr_getopt_t *o;
    apr_finfo_t info;
    apr_file_t *pidfile;
    int retries, isdaemon, limit_found, inodes_found, intelligent, dowork;
    char opt;
    const char *arg;
    char *proxypath, *path, *pidfilename;

    interrupted = 0;
    repeat = 0;
    isdaemon = 0;
    dryrun = 0;
    limit_found = 0;
    inodes_found = 0;
    max = 0;
    inodes = 0;
    round = 0;
    verbose = 0;
    realclean = 0;
    benice = 0;
    deldirs = 0;
    intelligent = 0;
    previous = 0; /* avoid compiler warning */
    proxypath = NULL;
    pidfilename = NULL;

    if (apr_app_initialize(&argc, &argv, NULL) != APR_SUCCESS) {
        return 1;
    }
    atexit(apr_terminate);

    if (argc) {
        shortname = apr_filepath_name_get(argv[0]);
    }

    if (apr_pool_create(&pool, NULL) != APR_SUCCESS) {
        return 1;
    }
    apr_pool_abort_set(oom, pool);
    apr_file_open_stderr(&errfile, pool);
    apr_file_open_stdout(&outfile, pool);
    apr_signal(SIGINT, setterm);
    apr_signal(SIGTERM, setterm);

    apr_getopt_init(&o, pool, argc, argv);

    while (1) {
        status = apr_getopt(o, "iDnvrtd:l:L:p:P:R:aA", &opt, &arg);
        if (status == APR_EOF) {
            break;
        }
        else if (status != APR_SUCCESS) {
            usage(NULL);
        }
        else {
            char *end;
            apr_status_t rv;
            switch (opt) {
            case 'i':
                if (intelligent) {
                    usage_repeated_arg(pool, opt);
                }
                intelligent = 1;
                break;

            case 'D':
                if (dryrun) {
                    usage_repeated_arg(pool, opt);
                }
                dryrun = 1;
                break;

            case 'n':
                if (benice) {
                    usage_repeated_arg(pool, opt);
                }
                benice = 1;
                break;

            case 't':
                if (deldirs) {
                    usage_repeated_arg(pool, opt);
                }
                deldirs = 1;
                break;

            case 'v':
                if (verbose) {
                    usage_repeated_arg(pool, opt);
                }
                verbose = 1;
                break;

            case 'r':
                if (realclean) {
                    usage_repeated_arg(pool, opt);
                }
                realclean = 1;
                deldirs = 1;
                break;

            case 'd':
                if (isdaemon) {
                    usage_repeated_arg(pool, opt);
                }
                isdaemon = 1;
                repeat = apr_atoi64(arg);
                repeat *= SECS_PER_MIN;
                repeat *= APR_USEC_PER_SEC;
                break;

            case 'l':
                if (limit_found) {
                    usage_repeated_arg(pool, opt);
                }
                limit_found = 1;

                do {
                    rv = apr_strtoff(&max, arg, &end, 10);
                    if (rv == APR_SUCCESS) {
                        if ((*end == 'K' || *end == 'k') && !end[1]) {
                            max *= KBYTE;
                        }
                        else if ((*end == 'M' || *end == 'm') && !end[1]) {
                            max *= MBYTE;
                        }
                        else if ((*end == 'G' || *end == 'g') && !end[1]) {
                            max *= GBYTE;
                        }
                        else if (*end &&        /* neither empty nor [Bb] */
                                 ((*end != 'B' && *end != 'b') || end[1])) {
                            rv = APR_EGENERAL;
                        }
                    }
                    if (rv != APR_SUCCESS) {
                        usage(apr_psprintf(pool, "Invalid limit: %s"
                                                 APR_EOL_STR APR_EOL_STR, arg));
                    }
                } while(0);
                break;

            case 'L':
                if (inodes_found) {
                    usage_repeated_arg(pool, opt);
                }
                inodes_found = 1;

                do {
                    rv = apr_strtoff(&inodes, arg, &end, 10);
                    if (rv == APR_SUCCESS) {
                        if ((*end == 'K' || *end == 'k') && !end[1]) {
                            inodes *= KBYTE;
                        }
                        else if ((*end == 'M' || *end == 'm') && !end[1]) {
                            inodes *= MBYTE;
                        }
                        else if ((*end == 'G' || *end == 'g') && !end[1]) {
                            inodes *= GBYTE;
                        }
                        else if (*end &&        /* neither empty nor [Bb] */
                                 ((*end != 'B' && *end != 'b') || end[1])) {
                            rv = APR_EGENERAL;
                        }
                    }
                    if (rv != APR_SUCCESS) {
                        usage(apr_psprintf(pool, "Invalid limit: %s"
                                                 APR_EOL_STR APR_EOL_STR, arg));
                    }
                } while(0);
                break;

            case 'a':
                if (listurls) {
                    usage_repeated_arg(pool, opt);
                }
                listurls = 1;
                break;

            case 'A':
                if (listurls) {
                    usage_repeated_arg(pool, opt);
                }
                listurls = 1;
                listextended = 1;
                break;

            case 'p':
                if (proxypath) {
                    usage_repeated_arg(pool, opt);
                }
                proxypath = apr_pstrdup(pool, arg);
                if ((status = apr_filepath_set(proxypath, pool)) != APR_SUCCESS) {
                    usage(apr_psprintf(pool, "Could not set filepath to '%s': %pm",
                                       proxypath, &status));
                }
                break;

            case 'P':
                if (pidfilename) {
                    usage_repeated_arg(pool, opt);
                }
                pidfilename = apr_pstrdup(pool, arg);
                break;

            case 'R':
                if (round) {
                    usage_repeated_arg(pool, opt);
                }
                rv = apr_strtoff(&round, arg, &end, 10);
                if (rv == APR_SUCCESS) {
                    if (*end) {
                        usage(apr_psprintf(pool, "Invalid round value: %s"
                                                 APR_EOL_STR APR_EOL_STR, arg));
                    }
                    else if (round < 0) {
                        usage(apr_psprintf(pool, "Round value must be positive: %s"
                                                 APR_EOL_STR APR_EOL_STR, arg));
                    }
                }
                if (rv != APR_SUCCESS) {
                    usage(apr_psprintf(pool, "Invalid round value: %s"
                                             APR_EOL_STR APR_EOL_STR, arg));
                }
                break;

            } /* switch */
        } /* else */
    } /* while */

    if (argc <= 1) {
        usage(NULL);
    }

    if (!proxypath) {
         usage("Option -p must be specified");
    }

    if (o->ind < argc) {
        int deleted = 0;
        int error = 0;
        if (isdaemon) {
            usage("Option -d cannot be used with URL arguments, aborting");
        }
        if (intelligent) {
            usage("Option -i cannot be used with URL arguments, aborting");
        }
        if (limit_found) {
            usage("Option -l cannot be used with URL arguments, aborting");
        }
        while (o->ind < argc) {
            status = delete_url(pool, proxypath, argv[o->ind]);
            if (APR_SUCCESS == status) {
                if (verbose) {
                    apr_file_printf(errfile, "Removed: %s" APR_EOL_STR,
                            argv[o->ind]);
                }
                deleted = 1;
            }
            else if (APR_ENOENT == status) {
                if (verbose) {
                    apr_file_printf(errfile, "Not cached: %s" APR_EOL_STR,
                            argv[o->ind]);
                }
            }
            else {
                if (verbose) {
                    apr_file_printf(errfile, "Error while removed: %s" APR_EOL_STR,
                            argv[o->ind]);
                }
                error = 1;
            }
            o->ind++;
        }
        return error ? 1 : deleted ? 0 : 2;
    }

    if (isdaemon && repeat <= 0) {
         usage("Option -d must be greater than zero");
    }

    if (isdaemon && (verbose || realclean || dryrun || listurls)) {
         usage("Option -d cannot be used with -v, -r, -L or -D");
    }

    if (!isdaemon && intelligent) {
         usage("Option -i cannot be used without -d");
    }

    if (!listurls && max <= 0 && inodes <= 0) {
         usage("At least one of option -l or -L must be greater than zero");
    }

    if (apr_filepath_get(&path, 0, pool) != APR_SUCCESS) {
        usage(apr_psprintf(pool, "Could not get the filepath: %pm", &status));
    }
    baselen = strlen(path);

    if (pidfilename) {
        log_pid(pool, pidfilename, &pidfile); /* before daemonizing, so we
                                               * can report errors
                                               */
    }

    if (listurls) {
        list_urls(path, pool, round);
        return (interrupted != 0);
    }

#ifndef DEBUG
    if (isdaemon) {
        apr_file_close(errfile);
        errfile = NULL;
        if (pidfilename) {
            apr_file_close(pidfile); /* delete original pidfile only in parent */
        }
        apr_proc_detach(APR_PROC_DETACH_DAEMONIZE);
        if (pidfilename) {
            log_pid(pool, pidfilename, &pidfile);
        }
    }
#endif

    do {
        apr_pool_create(&instance, pool);

        now = apr_time_now();
        APR_RING_INIT(&root, _entry, link);
        delcount = 0;
        unsolicited = 0;
        dowork = 0;

        switch (intelligent) {
        case 0:
            dowork = 1;
            break;

        case 1:
            retries = STAT_ATTEMPTS;
            status = APR_SUCCESS;

            do {
                if (status != APR_SUCCESS) {
                    apr_sleep(STAT_DELAY);
                }
                status = apr_stat(&info, path, APR_FINFO_MTIME, instance);
            } while (status != APR_SUCCESS && !interrupted && --retries);

            if (status == APR_SUCCESS) {
                previous = info.mtime;
                intelligent = 2;
            }
            dowork = 1;
            break;

        case 2:
            retries = STAT_ATTEMPTS;
            status = APR_SUCCESS;

            do {
                if (status != APR_SUCCESS) {
                    apr_sleep(STAT_DELAY);
                }
                status = apr_stat(&info, path, APR_FINFO_MTIME, instance);
            } while (status != APR_SUCCESS && !interrupted && --retries);

            if (status == APR_SUCCESS) {
                if (previous != info.mtime) {
                    dowork = 1;
                }
                previous = info.mtime;
                break;
            }
            intelligent = 1;
            dowork = 1;
            break;
        }

        if (dowork && !interrupted) {
            apr_off_t nodes = 0;
            if (!process_dir(path, instance, &nodes) && !interrupted) {
                purge(path, instance, max, inodes, nodes, round);
            }
            else if (!isdaemon && !interrupted) {
                apr_file_printf(errfile, "An error occurred, cache cleaning "
                                         "aborted." APR_EOL_STR);
                return 1;
            }

            if (intelligent && !interrupted) {
                retries = STAT_ATTEMPTS;
                status = APR_SUCCESS;
                do {
                    if (status != APR_SUCCESS) {
                        apr_sleep(STAT_DELAY);
                    }
                    status = apr_stat(&info, path, APR_FINFO_MTIME, instance);
                } while (status != APR_SUCCESS && !interrupted && --retries);

                if (status == APR_SUCCESS) {
                    previous = info.mtime;
                    intelligent = 2;
                }
                else {
                    intelligent = 1;
                }
            }
        }

        apr_pool_destroy(instance);

        current = apr_time_now();
        if (current < now) {
            delay = repeat;
        }
        else if (current - now >= repeat) {
            delay = repeat;
        }
        else {
            delay = now + repeat - current;
        }

        /* we can't sleep the whole delay time here apiece as this is racy
         * with respect to interrupt delivery - think about what happens
         * if we have tested for an interrupt, then get scheduled
         * before the apr_sleep() call and while waiting for the cpu
         * we do get an interrupt
         */
        if (isdaemon) {
            while (delay && !interrupted) {
                if (delay > APR_USEC_PER_SEC) {
                    apr_sleep(APR_USEC_PER_SEC);
                    delay -= APR_USEC_PER_SEC;
                }
                else {
                    apr_sleep(delay);
                    delay = 0;
                }
            }
        }
    } while (isdaemon && !interrupted);

    if (!isdaemon && interrupted) {
        apr_file_printf(errfile, "Cache cleaning aborted due to user "
                                 "request." APR_EOL_STR);
        return 1;
    }

    return 0;
}
