]> granicus.if.org Git - apache/commitdiff
Add htcacheclean to function as cleanup utility (daemonizable) for
authorJustin Erenkrantz <jerenkrantz@apache.org>
Sat, 6 Nov 2004 07:45:21 +0000 (07:45 +0000)
committerJustin Erenkrantz <jerenkrantz@apache.org>
Sat, 6 Nov 2004 07:45:21 +0000 (07:45 +0000)
mod_disk_cache.

(Justin did a whole bunch of style tweaks, and some minor functionality tweaks
to get it to function on Solaris.)

Submitted by: Andreas Steinmetz
Reviewed by: Justin Erenkrantz

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@105698 13f79535-47bb-0310-9956-ffa450edef68

CHANGES
support/.cvsignore
support/Makefile.in
support/htcacheclean.c [new file with mode: 0644]

diff --git a/CHANGES b/CHANGES
index 376b31188c27fe1d41decd8ac058b87fc54d6c1e..03a73def8057de2a40e0bf87dac28273361091aa 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -2,11 +2,14 @@ Changes with Apache 2.1.0-dev
 
   [Remove entries to the current 2.0 section below, when backported]
 
+  *) Add htcacheclean to support/ for assistance with mod_disk_cache.
+     [Andreas Steinmetz]
+
   *) mod_authnz_ldap: Added the directive "Requires ldap-filter" that
      allows the module to authorize a user based on a complex LDAP
      search filter.
      [Brad Nicholes]
-     
+
   *) SECURITY: CAN-2004-0942, Fix for memory consumption DoS.
      [Joe Orton]
 
@@ -17,7 +20,7 @@ Changes with Apache 2.1.0-dev
      allows the module to only authorize a user if the attribute value
      specified matches the value of the user object. PR 31913
      [Ryan Morgan <rmorgan pobox.com>]
-     
+
   *) Allow mod_authnz_ldap authorization functionality to be used 
      without requiring the user to also be authenticated through 
      mod_authnz_ldap. This allows other authentication modules to 
index e4018d42be910003f941fee530d09a3d0fafd33e..69916d9286ec6728abb81e9f4436be87e013dba7 100644 (file)
@@ -7,6 +7,7 @@ rotatelogs
 htpasswd
 htdbm
 htdigest
+htcacheclean
 unescape
 inc2shtml
 httpd_monitor
index 8fca31c52b3fa493049a80dec4784a1f6c42a0a2..25dda638c13f1e227ac62be0ea10ece543ad3f7b 100644 (file)
@@ -3,7 +3,7 @@ DISTCLEAN_TARGETS = apxs apachectl dbmmanage log_server_status \
 
 CLEAN_TARGETS = suexec
 
-PROGRAMS = htpasswd htdigest rotatelogs logresolve ab checkgid htdbm
+PROGRAMS = htpasswd htdigest rotatelogs logresolve ab checkgid htdbm htcacheclean
 TARGETS  = $(PROGRAMS)
 
 PROGRAM_LDADD        = $(UTIL_LDFLAGS) $(PROGRAM_DEPENDENCIES) $(EXTRA_LIBS) $(AP_LIBS)
@@ -62,3 +62,6 @@ suexec_OBJECTS = suexec.lo
 suexec: $(suexec_OBJECTS)
        $(LINK) $(suexec_OBJECTS)
 
+htcacheclean_OBJECTS = htcacheclean.lo
+htcacheclean: $(htcacheclean_OBJECTS)
+       $(LINK) $(htcacheclean_LTFLAGS) $(htcacheclean_OBJECTS) $(PROGRAM_LDADD)
diff --git a/support/htcacheclean.c b/support/htcacheclean.c
new file mode 100644 (file)
index 0000000..7a8227b
--- /dev/null
@@ -0,0 +1,1013 @@
+/* Copyright 2001-2004 The Apache Software Foundation
+ *
+ * Licensed 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_ring.h"
+#include "apr_date.h"
+
+#if APR_HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+#if APR_HAVE_STDLIB_H
+#include <stdlib.h>
+#endif
+
+/* mod_disk_cache.c extract start */
+
+#define DISK_FORMAT_VERSION 0
+typedef struct {
+    /* Indicates the format of the header struct stored on-disk. */
+    int format;
+    /* The HTTP status code returned for this response.  */
+    int status;
+    /* The size of the entity name that follows. */
+    apr_size_t name_len;
+    /* The number of times we've cached this entity. */
+    apr_size_t entity_version;
+    /* Miscellaneous time values. */
+    apr_time_t date;
+    apr_time_t expire;
+    apr_time_t request_time;
+    apr_time_t response_time;
+} disk_cache_info_t;
+
+#define CACHE_HEADER_SUFFIX ".header"
+#define CACHE_DATA_SUFFIX   ".data"
+/* mod_disk_cache.c extract end */
+
+/* mod_disk_cache.c related definitions start */
+
+/*
+ * this is based on #define AP_TEMPFILE "/aptmpXXXXXX"
+ *
+ * the above definition could be reworked into the following:
+ *
+ * #define AP_TEMPFILE_PREFIX "/"
+ * #define AP_TEMPFILE_BASE   "aptmp"
+ * #define AP_TEMPFILE_SUFFIX "XXXXXX"
+ * #define AP_TEMPFILE_BASELEN strlen(AP_TEMPFILE_BASE)
+ * #define AP_TEMPFILE_NAMELEN strlen(AP_TEMPFILE_BASE AP_TEMPFILE_SUFFIX)
+ * #define AP_TEMPFILE AP_TEMPFILE_PREFIX AP_TEMPFILE_BASE AP_TEMPFILE_SUFFIX
+ *
+ * these definitions would then match the definitions below:
+ */
+
+#define AP_TEMPFILE_BASE    "aptmp"
+#define AP_TEMPFILE_SUFFIX  "XXXXXX"
+#define AP_TEMPFILE_BASELEN strlen(AP_TEMPFILE_BASE)
+#define AP_TEMPFILE_NAMELEN strlen(AP_TEMPFILE_BASE AP_TEMPFILE_SUFFIX)
+
+/* mod_disk_cache.c related definitions end */
+
+/* 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 DIRINFO (APR_FINFO_MTIME|APR_FINFO_SIZE|APR_FINFO_TYPE|APR_FINFO_LINK)
+
+typedef struct _direntry
+{
+    APR_RING_ENTRY(_direntry) link;
+    /* type of file/fileset: TEMP, HEADER, DATA, HEADERDATA */
+    int type;
+    /* headers file modification time */
+    apr_time_t htime;
+    /* body file modification time */
+    apr_time_t dtime;
+    /* headers file size */
+    apr_off_t hsize;
+    /* body or temporary file size */
+    apr_off_t dsize;
+    /* file/fileset base name */
+    char *basename;
+} DIRENTRY;
+
+typedef struct _entry
+{
+    APR_RING_ENTRY(_entry) link;
+    /* cache entry exiration time */
+    apr_time_t expire;
+    /* cache entry time of last response to client */
+    apr_time_t response_time;
+    /* headers file modification time */
+    apr_time_t htime;
+    /* body file modification time */
+    apr_time_t dtime;
+    /* headers file size */
+    apr_off_t hsize;
+    /* body or temporary file size */
+    apr_off_t dsize;
+    /* fileset base name */
+    char *basename;
+} ENTRY;
+
+/* file deletion count for nice mode */
+static int delcount;
+/* flag: true if SIGINT or SIGTERM occurred */
+static int interrupted;
+/* flag: true means user said apache is not running */
+static int realclean;
+/* flag: true means print statistics */
+static int verbose;
+/* flag: true means nice mode is activated */
+static int benice;
+/* flag: true means dry run, don't actually delete anything */
+static int dryrun;
+/* string length of the path to the proxy directory */
+static int baselen;
+/* start time of this processing run */
+static apr_time_t now;
+/* stderr file handle */
+static apr_file_t *errfile;
+/* file size summary for deleted unsolicited files */
+static apr_off_t unsolicited;
+/* ENTRY ring anchor */
+static APR_RING_ENTRY(_entry) root;
+
+/*
+ * fake delete for debug purposes
+ */
+
+#ifdef DEBUG
+#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\n", pathname);
+}
+#endif
+
+/*
+ * called on SIGINT or SIGTERM
+ */
+
+static void setterm(int unused)
+{
+#ifdef DEBUG
+    apr_file_printf(errfile, "interrupt\n");
+#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(apr_off_t total, apr_off_t sum, apr_off_t max,
+                       apr_off_t etotal, apr_off_t entries)
+{
+    char ttype;
+    char stype;
+    char mtype;
+    char utype;
+    apr_off_t tfrag;
+    apr_off_t sfrag;
+    apr_off_t ufrag;
+
+    if (!verbose) {
+        return;
+    }
+
+    ttype = 'K';
+    tfrag = ((total * 10) / KBYTE) % 10;
+    total /= KBYTE;
+    if (total >= KBYTE) {
+        ttype = 'M';
+        tfrag = ((total * 10) / KBYTE) % 10;
+        total /= KBYTE;
+    }
+
+    stype = 'K';
+    sfrag = ((sum * 10) / KBYTE) % 10;
+    sum /= KBYTE;
+    if (sum >= KBYTE) {
+        stype = 'M';
+        sfrag = ((sum * 10) / KBYTE) % 10;
+        sum /= KBYTE;
+    }
+
+    mtype = 'K';
+    max /= KBYTE;
+    if (max >= KBYTE) {
+        mtype = 'M';
+        max /= KBYTE;
+    }
+
+    apr_file_printf(errfile, "Statistics:\n");
+    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\n",
+            (int)(unsolicited), (int)(ufrag), utype);
+     }
+     apr_file_printf(errfile, "size limit %d.0%c\n", (int)(max), mtype);
+     apr_file_printf(errfile,
+         "total size was %d.%d%c, total size now %d.%d%c\n",
+         (int)(total), (int)(tfrag), ttype, (int)(sum), (int)(sfrag), stype);
+     apr_file_printf(errfile, "total entries was %d, total entries now %d\n",
+         (int)(etotal), (int)(entries));
+}
+
+/*
+ * delete a single file
+ */
+static void delete_file(char *path, char *basename, apr_pool_t *pool)
+{
+     char *nextpath;
+     apr_pool_t *p;
+
+     if (dryrun) {
+         return;
+     }
+
+     /* temp pool, otherwise lots of memory could be allocated */
+     apr_pool_create(&p, pool);
+     nextpath = apr_pstrcat(p, path, "/", basename, NULL);
+     apr_file_remove(nextpath, p);
+     apr_pool_destroy(p);
+
+     if (benice) {
+         if (++delcount >= DELETE_NICE) {
+             apr_sleep(NICE_DELAY);
+             delcount = 0;
+         }
+     }
+}
+
+/*
+ * delete cache file set
+ */
+static void delete_entry(char *path, char *basename, apr_pool_t *pool)
+{
+     char *nextpath;
+     apr_pool_t *p;
+
+     if (dryrun) {
+         return;
+     }
+
+     /* temp pool, otherwise lots of memory could be allocated */
+     apr_pool_create(&p, pool);
+
+     nextpath = apr_pstrcat(p, path, "/", basename, CACHE_HEADER_SUFFIX, NULL);
+     apr_file_remove(nextpath, p);
+
+     nextpath = apr_pstrcat(p, path, "/", basename, CACHE_DATA_SUFFIX, NULL);
+     apr_file_remove(nextpath, p);
+
+     apr_pool_destroy(p);
+
+     if (benice) {
+         delcount += 2;
+         if (delcount >= DELETE_NICE) {
+             apr_sleep(NICE_DELAY);
+             delcount = 0;
+         }
+     }
+}
+
+/*
+ * walk the cache directory tree
+ */
+static int process_dir(char *path, apr_pool_t *pool)
+{
+     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;
+     apr_time_t deviation;
+     char *nextpath;
+     char *base;
+     char *ext;
+     APR_RING_ENTRY(_direntry) anchor;
+     DIRENTRY *d;
+     DIRENTRY *t;
+     DIRENTRY *n;
+     ENTRY *e;
+     int skip;
+     int 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;
+     skip = 0;
+     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) {
+         /* skip first two entries which will always be '.' and '..' */
+         if (skip < 2) {
+             skip++;
+             continue;
+         }
+         d = apr_pcalloc(p, sizeof(DIRENTRY));
+         d->basename = apr_pstrcat(p, path, "/", info.name, NULL);
+         APR_RING_INSERT_TAIL(&anchor, d, _direntry, link);
+     }
+
+     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 errno 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) {
+             if (process_dir(d->basename, pool)) {
+                 return 1;
+             }
+             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)) {
+        apr_hash_this(i, NULL, NULL, (void **)(&d));
+        switch(d->type) {
+        case HEADERDATA:
+            nextpath = apr_pstrcat(p, path, "/", d->basename,
+                CACHE_HEADER_SUFFIX, NULL);
+            if (apr_file_open(&fd, nextpath, APR_READ, APR_OS_DEFAULT, p)
+              == APR_SUCCESS) {
+                len = sizeof(disk_cache_info_t);
+                if (apr_file_read_full(fd, &disk_info, len, &len)
+                  == APR_SUCCESS) {
+                    apr_file_close(fd);
+                    if (disk_info.format == DISK_FORMAT_VERSION) {
+                        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_palloc(pool,
+                                                 strlen(d->basename) + 1);
+                        strcpy(e->basename, d->basename);
+                        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, 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();
+            if (realclean || d->htime < current - deviation ||
+                 d->htime > current + deviation) {
+                 delete_entry(path, d->basename, 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, 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, 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 sum;
+    apr_off_t total;
+    apr_off_t entries;
+    apr_off_t etotal;
+    ENTRY *e;
+    ENTRY *n;
+    ENTRY *oldest;
+
+    sum = 0;
+    entries = 0;
+
+    for (e = APR_RING_FIRST(&root);
+         e != APR_RING_SENTINEL(&root, _entry, link);
+         e = APR_RING_NEXT(e, link)) {
+        sum += e->hsize;
+        sum += e->dsize;
+        entries++;
+    }
+
+    total = sum;
+    etotal = entries;
+
+    if (sum <= max) {
+        printstats(total, sum, max, etotal, entries);
+        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, pool);
+            sum -= e->hsize;
+            sum -= e->dsize;
+            entries--;
+            APR_RING_REMOVE(e, link);
+            if (sum <= max) {
+                if (!interrupted) {
+                    printstats(total, sum, max, etotal, entries);
+                }
+                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, pool);
+            sum -= e->hsize;
+            sum -= e->dsize;
+            entries--;
+            APR_RING_REMOVE(e, link);
+            if (sum <= max) {
+                if (!interrupted)
+                    printstats(total, sum, max, etotal, entries);
+                return;
+            }
+        }
+        e = n;
+    }
+
+    if (interrupted) {
+         return;
+    }
+
+    /* process remaining entries oldest to newest, the check for an emtpy
+     * ring actually isn't necessary except when the compiler does
+     * corrupt 64bit arithmetics which happend to me once, so better safe
+     * than sorry
+     */
+    while (sum > max && !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, pool);
+        sum -= oldest->hsize;
+        sum -= oldest->dsize;
+        entries--;
+        APR_RING_REMOVE(oldest, link);
+    }
+
+    if (!interrupted) {
+        printstats(total, sum, max, etotal, entries);
+    }
+}
+
+/*
+ * usage info
+ */
+static void usage(void)
+{
+    apr_file_printf(errfile, "htcacheclean -- program for cleaning the "
+                             "disk cache.\n");
+    apr_file_printf(errfile, "Usage: htcacheclean [-Dvrn] -pPATH -lLIMIT\n");
+    apr_file_printf(errfile, "       htcacheclean [-Dvrn] -pPATH -LLIMIT\n");
+    apr_file_printf(errfile, "       htcacheclean [-ni] -dINTERVAL -pPATH "
+                             "-lLIMIT\n");
+    apr_file_printf(errfile, "       htcacheclean [-ni] -dINTERVAL -pPATH "
+                             "-LLIMIT\n");
+    apr_file_printf(errfile, "Options:\n");
+    apr_file_printf(errfile, "   -d   Daemonize and repeat cache cleaning "
+                             "every INTERVAL minutes. This\n"
+                             "        option is mutually exclusive with "
+                             "the -D, -v and -r options.\n");
+    apr_file_printf(errfile, "   -D   Do a dry run and don't delete anything. "
+                             "This option is mutually\n"
+                             "        exclusive with the -d option.\n");
+    apr_file_printf(errfile, "   -v   Be verbose and print statistics. "
+                             "This option is mutually exclusive\n"
+                             "        with the -d option.\n");
+    apr_file_printf(errfile, "   -r   Clean thoroughly. This assumes that "
+                             "the Apache web server\n"
+                             "        is not running. This option is "
+                             "mutually exclusive with the -d option.\n");
+    apr_file_printf(errfile, "   -n   Be nice. This causes slower processing "
+                             "in favour of other processes.\n");
+    apr_file_printf(errfile, "   -p   Specify PATH as the root directory of "
+                             "the disk cache.\n");
+    apr_file_printf(errfile, "   -l   Specify LIMIT as the total disk cache "
+                             "size limit in KBytes.\n");
+    apr_file_printf(errfile, "   -L   Specify LIMIT as the total disk cache "
+                             "size limit in MBytes.\n");
+    apr_file_printf(errfile, "   -i   Be intelligent and run only when there "
+                             "was a modification\n"
+                             "        of the disk cache. This option is only "
+                             "possible together with\n"
+                             "        the -d option.\n");
+    exit(1);
+}
+
+/*
+ * main
+ */
+int main(int argc, const char * const argv[])
+{
+    apr_off_t max;
+    apr_time_t current;
+    apr_time_t repeat;
+    apr_time_t delay;
+    apr_time_t previous;
+    apr_status_t status;
+    apr_pool_t *pool;
+    apr_pool_t *instance;
+    apr_getopt_t *o;
+    apr_finfo_t info;
+    int retries;
+    int isdaemon;
+    int limit_found;
+    int intelligent;
+    int dowork;
+    char opt;
+    const char *arg;
+    char *proxypath;
+    char *path;
+
+    interrupted = 0;
+    repeat = 0;
+    isdaemon = 0;
+    dryrun = 0;
+    limit_found = 0;
+    max = 0;
+    verbose = 0;
+    realclean = 0;
+    benice = 0;
+    intelligent = 0;
+    proxypath = NULL;
+
+    if (apr_app_initialize(&argc, &argv, NULL) != APR_SUCCESS) {
+        return 1;
+    }
+    atexit(apr_terminate);
+
+    if (apr_pool_create(&pool, NULL) != APR_SUCCESS) {
+        return 1;
+    }
+    apr_pool_abort_set(oom, pool);
+    apr_file_open_stderr(&errfile, pool);
+    apr_signal(SIGINT, setterm);
+    apr_signal(SIGTERM, setterm);
+
+    apr_getopt_init(&o, pool, argc, argv);
+
+    while (1) {
+        status = apr_getopt(o, "iDnvrd:l:L:p:", &opt, &arg);
+        if (status == APR_EOF)
+            break;
+        else if (status == APR_SUCCESS)
+          switch (opt) {
+            case 'i':
+                if (intelligent)
+                    usage();
+                intelligent = 1;
+                break;
+            case 'D':
+                if (dryrun)
+                    usage();
+                 dryrun = 1;
+               break;
+            case 'n':
+                if (benice)
+                    usage();
+                benice = 1;
+                 break;
+           case 'v':
+                 if (verbose)
+                    usage();
+                verbose = 1;
+               break;
+             case 'r':
+               if (realclean)
+                     usage();
+                realclean = 1;
+                break;
+            case 'd':
+                if (isdaemon)
+                    usage();
+                isdaemon = 1;
+                repeat = apr_atoi64(arg);
+                repeat *= SECS_PER_MIN;
+                repeat *= APR_USEC_PER_SEC;
+                break;
+            case 'l':
+                if (limit_found)
+                    usage();
+                limit_found = 1;
+                max = apr_atoi64(arg);
+                max *= KBYTE;
+                break;
+            case 'L':
+                if (limit_found)
+                    usage();
+                limit_found = 1;
+                max = apr_atoi64(arg);
+                max *= MBYTE;
+               break;
+             case 'p':
+                if (proxypath)
+                    usage();
+               proxypath = apr_pstrdup(pool, arg);
+                 if (apr_filepath_set(proxypath, pool) != APR_SUCCESS)
+                    usage();
+                break;
+        }
+        else usage();
+    }
+
+    if (o->ind != argc) {
+         usage();
+    }
+
+    if (isdaemon && (repeat <= 0 || verbose || realclean || dryrun)) {
+         usage();
+    }
+
+    if (!isdaemon && intelligent) {
+         usage();
+    }
+
+    if (!proxypath || max <= 0) {
+         usage();
+    }
+
+    if (apr_filepath_get(&path, 0, pool) != APR_SUCCESS) {
+        usage();
+    }
+    baselen = strlen(path);
+
+#ifndef DEBUG
+    if (isdaemon) {
+        apr_file_close(errfile);
+        apr_proc_detach(APR_PROC_DETACH_DAEMONIZE);
+    }
+#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) {
+            if (!process_dir(path, instance) && !interrupted) {
+               purge(path, instance, max);
+            } else if (!isdaemon && !interrupted) {
+                apr_file_printf(errfile,
+                     "An error occurred, cache cleaning aborted.\n");
+                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.\n");
+        return 1;
+    }
+
+    return 0;
+}