chiark / gitweb /
merge extra MIME parsing
authorRichard Kettlewell <rjk@greenend.org.uk>
Tue, 18 Dec 2007 10:40:52 +0000 (10:40 +0000)
committerRichard Kettlewell <rjk@greenend.org.uk>
Tue, 18 Dec 2007 10:40:52 +0000 (10:40 +0000)
101 files changed:
.bzrignore
Makefile.am
README
README.streams
README.upgrades
acinclude.m4
clients/Makefile.am
clients/disorder.c
clients/disorderfm.c
clients/dump2wav [new file with mode: 0755]
clients/playrtp-alsa.c
clients/playrtp-coreaudio.c
clients/playrtp-oss.c
clients/playrtp.c
clients/playrtp.h
configure.ac
debian/changelog
debian/conffiles.disorder [new file with mode: 0644]
debian/rules
disobedience/Makefile.am
disobedience/disobedience.c
disobedience/menu.c
doc/disobedience.1.in
doc/disorder-dbupgrade.8.in
doc/disorder-deadlock.8.in
doc/disorder-decode.8.in
doc/disorder-dump.8.in
doc/disorder-normalize.8
doc/disorder-playrtp.1.in
doc/disorder-rescan.8.in
doc/disorder-speaker.8
doc/disorder-stats.8.in
doc/disorder.1.in
doc/disorder.3
doc/disorder_config.5.in
doc/disorder_protocol.5.in
doc/disorderd.8.in
doc/disorderfm.1.in
doc/tkdisorder.1
lib/Makefile.am
lib/addr.c
lib/addr.h
lib/basen.c
lib/configuration.c
lib/configuration.h
lib/defs.c
lib/defs.h
lib/eclient.c
lib/filepart.c
lib/filepart.h
lib/mime.c
lib/printf.c
lib/queue.h
lib/selection.c
lib/selection.h
lib/sink.c
lib/sink.h
lib/split.c
lib/test.c
lib/utf8.c [deleted file]
lib/utf8.h [deleted file]
lib/wstat.c
lib/wstat.h
python/disorder.py.in
scripts/Makefile.am
scripts/dist
scripts/format-gcov-report [new file with mode: 0755]
scripts/make-version-string [new file with mode: 0755]
scripts/text2c
server/Makefile.am
server/dbupgrade.c
server/dcgi.c
server/deadlock.c
server/decode.c
server/disorderd.c
server/dump.c
server/normalize.c
server/play.c
server/rescan.c
server/server-queue.c
server/server.c
server/speaker-command.c
server/speaker-network.c
server/speaker-oss.c
server/speaker.c
server/speaker.h
server/state.c
server/stats.c
server/trackdb.c
server/trackdb.h
server/trackname.c
tests/Makefile.am
tests/dbversion.py
tests/dtest.py
tests/dump.py [new file with mode: 0755]
tests/files.py
tests/nothing.py [deleted file]
tests/play.py [new file with mode: 0755]
tests/queue.py [new file with mode: 0755]
tests/udplog.c [new file with mode: 0644]
tests/version.py [deleted file]

index 1b45fb1d88ffb4c5a04395a32f6df2a80ba2eaf0..0ca950c28f44facdaba2a4938967e0d8494b8c99 100644 (file)
@@ -130,3 +130,15 @@ tests/*.log
 server/disorder-dbupgrade
 doc/disorder-dbupgrade.8.html
 doc/disorder-dbupgrade.8
+*.gcov
+*.gcno
+*.gcda
+*.c.html
+clients/index.html
+disobedience/index.html
+lib/index.html
+server/index.html
+.DS_Store
+tests/disorder-udplog
+lib/version-string
+lib/version.h
index 0b6f5a744a3c3f09b2d9e8dfa2206d6b7e533c8e..b8318a4109ce0cf7309383900d0c93b771141292 100644 (file)
@@ -22,6 +22,19 @@ EXTRA_DIST=TODO CHANGES README.streams BUGS ChangeLog.d      \
 README.upgrades README.client README.mac README.raw
 SUBDIRS=@subdirs@
 
+check-report: before-check check make-coverage-reports
+before-check:
+       rm -f */*.gcda */*.gcov
+make-coverage-reports:
+       @for subdir in lib plugins server clients disobedience; do \
+         echo cd $$subdir;\
+         cd $$subdir;\
+         echo ${GCOV} *.c | ${PYTHON} ../scripts/format-gcov-report --html . *.c ;\
+         ${GCOV} *.c | ${PYTHON} ../scripts/format-gcov-report --html . *.c ;\
+         echo cd ..;\
+         cd ..;\
+       done
+
 dist-hook:
        bzr log > ${distdir}/ChangeLog.d/bzr-changelog
 
diff --git a/README b/README
index b61ac7a22252301c028043ad4de9b189c355c377..f6872cb08f05d0faefc2b80ebe936595357fd663 100644 (file)
--- a/README
+++ b/README
@@ -15,9 +15,9 @@ DisOrder is a multi-user software jukebox.
 
 See CHANGES for details of recent changes to DisOrder.
 
-The server supports Linux and can be made to on a Mac (see README.mac).  The
-clients work on both Linux and the Mac.  It could probably be ported to some
-other UNIX variants without too much effort.  Things you will need:
+The server supports Linux and can be made to work on a Mac (see README.mac).
+The clients work on both Linux and the Mac.  It could probably be ported to
+some other UNIX variants without too much effort.  Things you will need:
 
 Build dependencies:
   Name             Tested              Notes
@@ -73,6 +73,9 @@ Installation
 
 NOTE: If you are upgrading from an earlier version, see README.upgrades.
 
+On a Debian system, if you install from .deb files then you should be able to
+skip steps 1 to 6 and configure it via debconf.  This is strongly recommended!
+
 1. Build the software.  Do something like this:
 
      ./configure --sysconfdir=/etc --localstatedir=/var
@@ -145,13 +148,15 @@ NOTE: If you are upgrading from an earlier version, see README.upgrades.
    start up correctly there should be an error message.  Correct the problem
    and try again.
 
-7. After a minute it should start to play something.  Try scratching it:
+7. After a minute it should start to play something.  Try scratching it (as
+   root):
 
      disorder scratch
 
    The track should stop playing, and (if you set any up) a scratch sound play.
 
-8. Add any other users you want.  These easiest way to do this is:
+8. Add any other users you want.  These easiest way to do this is (still as
+   root):
 
      disorder authorize USERNAME
 
@@ -290,6 +295,8 @@ Copyright
 
 DisOrder - select and play digital audio files
 Copyright (C) 2003-2007 Richard Kettlewell
+Portions copyright (C) 2007 Ross Younger
+Portions copyright (C) 2007 Mark Wooding
 Portions extracted from MPG321, http://mpg321.sourceforge.net/
   Copyright (C) 2001 Joe Drew
   Copyright (C) 2000-2001 Robert Leslie
index 0a2676ce0b20cb74d6886d415074df3fb61aa296..9fe3e3af82de02220ed7f793a7a497c2405e698c 100644 (file)
@@ -1,7 +1,7 @@
 * Introduction
 
-This file describes DisOrder's relationship to various things that get called
-'streams'.
+This file describes DisOrder's relationship to several different things that
+get called 'streams'.
 
 * RTP Streaming
 
@@ -18,24 +18,26 @@ backend:
     broadcast_from 172.17.207.2 9002
 
 The destination address (broadcast) can be:
-   - a unicast address if you only want to talk to one client
    - a broadcast address for a local network
    - a multicast address
 
-The source address (broadcast_from) is optional but may be convenient in som
-ecases.
+The source address (broadcast_from) is optional but may be convenient in some
+cases.
 
 If the destination is a multicast address then you should set the TTL, for
 instance:
 
    multicast_ttl 10
 
+(The destination can also be a unicast address but that's not a tested
+configuration.)
+
 ** Playing The Stream
 
 To play, use the disorder-playrtp client.  If the destination address was a
 unicast or broadcast address then:
 
-   disorder-playrtp 0.0.0.0 9003
+   disorder-playrtp 9003
 
 If the destination address was a multicast address then you must specify that,
 for instance:
@@ -53,7 +55,7 @@ it is installed); look for the speaker icon.  If it detects that the server is
 using network play then its volume control will apply to the local volume, not
 the server's volume.
 
-If you run into trouble look for *.log files in ~/.disorder.
+If you run into trouble look for *.log files in the ~/.disorder directory.
 
 ** Limitations
 
@@ -65,7 +67,7 @@ any success with wireless.
 Possibly other lower-quality but lower-bandwidth encodings will be supported in
 future.
 
-If you have a too-recent version of sox you may need to set the sox_generation
+If you have a very recent version of sox you may need to set the sox_generation
 option.  See disorder_config(5).
 
 
@@ -89,35 +91,34 @@ where ices.xml is:
       <loglevel>4</loglevel>
       <consolelog>0</consolelog>
       <stream>
-         <metadata>
-             <name>lyonesse</name>
-             <genre>Various</genre>
-             <description>lyonesse disorder output</description>
-         </metadata>
-         <input>
-             <module>stdinpcm</module>
-             <param name="rate">44100</param>
-             <param name="channels">2</param>
-             <param name="metadata">1</param>
-             <param name="metadatafilename">/var/disorder/icedata</param>
-         </input>
-         <instance>
-             <hostname>lyonesse.anjou.terraraq.org.uk</hostname>
-             <port>8000</port>
-             <password>SOURCE PASSWORD HERE</password>
-             <mount>/disorder.ogg</mount>
-             <reconnectdelay>2</reconnectdelay>
-             <reconnectattempts>5</reconnectattempts>
-             <maxqueuelength>80</maxqueuelength>
-             <encode>
-                 <nominal-bitrate>64000</nominal-bitrate>
-                 <samplerate>44100</samplerate>
-                 <channels>2</channels>
-                 <flush-samples>8820</flush-samples>
-             </encode>
-         </instance>
-
-         </stream>
+          <metadata>
+              <name>lyonesse</name>
+              <genre>Various</genre>
+              <description>lyonesse disorder output</description>
+          </metadata>
+          <input>
+              <module>stdinpcm</module>
+              <param name="rate">44100</param>
+              <param name="channels">2</param>
+              <param name="metadata">1</param>
+              <param name="metadatafilename">/var/disorder/icedata</param>
+          </input>
+          <instance>
+              <hostname>lyonesse.anjou.terraraq.org.uk</hostname>
+              <port>8000</port>
+              <password>SOURCE PASSWORD HERE</password>
+              <mount>/disorder.ogg</mount>
+              <reconnectdelay>2</reconnectdelay>
+              <reconnectattempts>5</reconnectattempts>
+              <maxqueuelength>80</maxqueuelength>
+              <encode>
+                  <nominal-bitrate>64000</nominal-bitrate>
+                  <samplerate>44100</samplerate>
+                  <channels>2</channels>
+                  <flush-samples>8820</flush-samples>
+              </encode>
+          </instance>
+      </stream>
   </ices>
 
 This doesn't seem to get on very well with pausing but you're unlikely to want
@@ -128,6 +129,9 @@ behind the 'current' time, watch this space for a fix (or contribute one!)
 If you have a too-recent version of sox you may need to set the sox_generation
 option.
 
+Mark Wooding contributed the original support for this but it's been modified
+enough that he probably shouldn't be blamed for any bugs in the current code.
+
 
 * DisOrder and Republishing Internet Streams
 
index a79767b0108f9b39a938ede9d2816ab68d4a177d..915a79416593eb266cc727dadbce86aa7c5db5f5 100644 (file)
@@ -2,9 +2,8 @@
 
 The general procedure is:
 
- * stop the old daemon, e.g. with
-     /etc/init.d/disorder stop
- * back up your database
+ * stop the old daemon:  /etc/init.d/disorder stop
+ * back up your database directory (example below)
  * build and install the new version as described in the README
  * update the configuration files (see below)
  * start the new daemon, e.g. with
@@ -15,15 +14,28 @@ upgrading between particular versions.  Minor versions are not
 explicitly mentioned; a version number like 1.1 implicitly includes
 all 1.1.x versions.
 
-* 1.5 -> 2.0
+* 1.4/1.5 -> 2.0
 
-** Database upgrade
+** 'transform' and 'namepart' directives
 
-The first thing the server does when upgrading from 1.5 is run the
-disorder-dbupgrade program.  This is necessary to modify any non-ASCII track
-names to meet the latest version's stricter normalization practices.  The
-upgrade should succeed automatically; if not it should leave an error message
-in syslog.
+'transform' has moved from the web options to the main configuration file, so
+that they can be used by other interfaces.  The syntax and semantics are
+unchanged.
+
+More importantly however both 'transform' and 'namepart' are now optional, with
+sensible defaults being built in.  So if you were already using the default
+values you can just delete all instances of both.
+
+See disorder_config(5) for the default values.  Hopefuly they will be suitable
+for many configurations.  Please do send feedback.
+
+** 'enabled' and 'random_enabled' directives
+
+These have been removed.  Instead the state persists from one run of the server
+to the next.  If they appear in your configuration file they must be removed;
+the server will not start if they are present.
+
+** Database upgrade
 
 It is strongly recommended that you back up your database before performing the
 upgrade.  For example, as root, with the server STOPPED:
@@ -36,20 +48,11 @@ To restore, again as root:
   rm *
   cp -p BACKUP/* .
 
-** 'transform' and 'namepart' directives
-
-'transform' has moved from the web options to the main configuration file, so
-that they can be used by other interfaces.  The syntax and semantics are
-unchanged.
-
-More importantly however both 'transform' and 'namepart' are now optional, with
-sensible defaults being built in.  So if you were already using the default
-values you can just delete all instances of both.
-
-** enabled' and 'random_enabled' directives
-
-These have been removed.  Instead the state persists from one run of the server
-to the next.
+The first thing the server does when upgrading from 1.5 is run the
+disorder-dbupgrade program.  This is necessary to modify any non-ASCII track
+names to meet the latest version's stricter normalization practices.  The
+upgrade should succeed automatically; if not it should leave an error message
+in syslog.
 
 * 1.3 -> 1.4
 
index 562872ad96a8c6e6ea0c03279f5e59a3df252fa6..96670f7dd96bdd061fa2c1a2f2433436db4d71f8 100644 (file)
@@ -102,3 +102,15 @@ AC_DEFUN([RJK_REQUIRE_PCRE_UTF8],[
     AC_MSG_ERROR([please rebuild your pcre library with --enable-utf8])
   fi
 ])
+
+AC_DEFUN([RJK_GCOV],[
+  GCOV=${GCOV:-true}
+  AC_ARG_WITH([gcov],
+              [AS_HELP_STRING([--with-gcov],
+                              [Enable coverage testing])],
+              [if test $withval = yes; then
+                 CFLAGS="${CFLAGS} -O0 -fprofile-arcs -ftest-coverage"
+                 GCOV=`echo $CC | sed s'/gcc/gcov/;s/ .*$//'`;
+               fi])
+  AC_SUBST([GCOV],[$GCOV])
+])
index 72a77396891e15671e5307bac4657f7e58dc0388..16f7437bc93f872191f162fffe0badc4bfd2ec13 100644 (file)
@@ -20,6 +20,7 @@
 
 bin_PROGRAMS=disorder disorderfm disorder-playrtp
 noinst_PROGRAMS=test-eclient filename-bytes
+noinst_SCRIPTS=dump2wav
 
 AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
 
@@ -53,7 +54,7 @@ install-exec-hook:
 
 check: check-help check-completions
 
-# check everything has working --help
+# check everything has working --help and --version
 check-help: all
        ./disorder --version > /dev/null
        ./disorder --help > /dev/null
@@ -82,3 +83,5 @@ check-completions: disorder
        diff -u ,commands ,completions
 
 CLEANFILES=,commands ,completions
+
+EXTRA_DIST=dump2wav
index 9513b05daf5f0dd62917d844da33d0e1aa8bc31d..7ee9865bad53e39dcd2cc58a7ded0bab7d9941de 100644 (file)
@@ -62,6 +62,7 @@ static const struct option options[] = {
   { "config", required_argument, 0, 'c' },
   { "debug", no_argument, 0, 'd' },
   { "local", no_argument, 0, 'l' },
+  { "no-per-user-config", no_argument, 0, 'N' },
   { "help-commands", no_argument, 0, 'H' },
   { 0, 0, 0, 0 }
 };
@@ -83,7 +84,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorder version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
@@ -543,7 +544,7 @@ int main(int argc, char **argv) {
   pcre_malloc = xmalloc;
   pcre_free = xfree;
   if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
-  while((n = getopt_long(argc, argv, "hVc:dHl", options, 0)) >= 0) {
+  while((n = getopt_long(argc, argv, "hVc:dHlN", options, 0)) >= 0) {
     switch(n) {
     case 'h': help();
     case 'H': help_commands();
@@ -551,6 +552,7 @@ int main(int argc, char **argv) {
     case 'c': configfile = optarg; break;
     case 'd': debugging = 1; break;
     case 'l': local = 1; break;
+    case 'N': config_per_user = 0; break;
     default: fatal(0, "invalid option");
     }
   }
index ccb2472783068fda484c13c2b5a47d80da06d5a4..d8dffb62c959aba1522523b86596acc650c32304 100644 (file)
@@ -129,7 +129,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorderfm version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
diff --git a/clients/dump2wav b/clients/dump2wav
new file mode 100755 (executable)
index 0000000..aec1056
--- /dev/null
@@ -0,0 +1,5 @@
+#! /bin/bash
+#
+# Usage: dump2wav DUMPFILE [sox format options] OUTPUT
+#
+exec sox -r 44100 -s -w -c 2 "$@"
index 263ab40045186ec7ddaeda7ff0bfac0ed8646c1e..142f1eef72823dbbfbd43c2e4aa1727a1c203888 100644 (file)
@@ -30,6 +30,7 @@
 #include <alsa/asoundlib.h>
 #include <assert.h>
 #include <pthread.h>
+#include <arpa/inet.h>
 
 #include "mem.h"
 #include "log.h"
@@ -141,6 +142,15 @@ static int playrtp_alsa_writei(const void *s, size_t n) {
   } else {
     /* Success */
     next_timestamp += frames_written * 2;
+    if(dump_buffer) {
+      snd_pcm_sframes_t count;
+      const int16_t *sp = s;
+      
+      for(count = 0; count < frames_written * 2; ++count) {
+        dump_buffer[dump_index++] = (int16_t)ntohs(*sp++);
+        dump_index %= dump_size;
+      }
+    }
     return 0;
   }
 }
index a22c8af8c24c0ccaabd53bd242fc3e8ff1473290..c0f18193eee5fe37d27a41b3c46032c3bc13a113 100644 (file)
@@ -67,6 +67,14 @@ static OSStatus adioproc
           samples_available = samplesOutLeft;
         next_timestamp += samples_available;
         samplesOutLeft -= samples_available;
+        if(dump_buffer) {
+          size_t n;
+
+          for(n = 0; n < samples_available; ++n) {
+            dump_buffer[dump_index++] = (int16_t)ntohs(ptr[n]);
+            dump_index %= dump_size;
+          }
+        }
         while(samples_available-- > 0)
           *samplesOut++ = (int16_t)ntohs(*ptr++) * (0.5 / 32767);
         /* We don't bother junking the packet - that'll be dealt with next time
@@ -82,6 +90,14 @@ static OSStatus adioproc
         next_timestamp += samples_available;
         samplesOut += samples_available;
         samplesOutLeft -= samples_available;
+        if(dump_buffer) {
+          size_t n;
+
+          for(n = 0; n < samples_available; ++n) {
+            dump_buffer[dump_index++] = 0;
+            dump_index %= dump_size;
+          }
+        }
       }
     }
     ++ab;
index 3c6312d41e38f03d1e4d716679b0dd92f25a2fd2..b40947d69db41092e42bf58365fd732248ddd991 100644 (file)
@@ -1,6 +1,7 @@
 /*
  * This file is part of DisOrder.
  * Copyright (C) 2007 Richard Kettlewell
+ * Portions copyright (C) 2007 Ross Younger
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -37,6 +38,7 @@
 #include <fcntl.h>
 #include <unistd.h>
 #include <errno.h>
+#include <arpa/inet.h>
 
 #include "mem.h"
 #include "log.h"
@@ -137,6 +139,15 @@ static int playrtp_oss_flush(void) {
       if(nbyteswritten < playrtp_oss_bufsize)
         error(0, "%s: short write (%d/%d)",
               device, nbyteswritten, playrtp_oss_bufsize);
+      if(dump_buffer) {
+        int count;
+        const int16_t *sp = (const int16_t *)playrtp_oss_buffer;
+        
+        for(count = 0; count < playrtp_oss_bufsize; count += sizeof(int16_t)) {
+          dump_buffer[dump_index++] = (int16_t)ntohs(*sp++);
+          dump_index %= dump_size;
+        }
+      }
       playrtp_oss_bufused = 0;
       return 0;
     }
index 9757be6566d6835da080646181a0799e6a72a369..484d265110470dc7fd9b200f874ee9e41ec50e4b 100644 (file)
@@ -69,6 +69,8 @@
 #include <sys/time.h>
 #include <sys/un.h>
 #include <unistd.h>
+#include <sys/mman.h>
+#include <fcntl.h>
 
 #include "log.h"
 #include "mem.h"
@@ -190,6 +192,27 @@ HEAP_DEFINE(pheap, struct packet *, lt_packet);
 /** @brief Control socket or NULL */
 const char *control_socket;
 
+/** @brief Buffer for debugging dump
+ *
+ * The debug dump is enabled by the @c --dump option.  It records the last 20s
+ * of audio to the specified file (which will be about 3.5Mbytes).  The file is
+ * written as as ring buffer, so the start point will progress through it.
+ *
+ * Use clients/dump2wav to convert this to a WAV file, which can then be loaded
+ * into (e.g.) Audacity for further inspection.
+ *
+ * All three backends (ALSA, OSS, Core Audio) now support this option.
+ *
+ * The idea is to allow the user a few seconds to react to an audible artefact.
+ */
+int16_t *dump_buffer;
+
+/** @brief Current index within debugging dump */
+size_t dump_index;
+
+/** @brief Size of debugging dump in samples */
+size_t dump_size = 44100/*Hz*/ * 2/*channels*/ * 20/*seconds*/;
+
 static const struct option options[] = {
   { "help", no_argument, 0, 'h' },
   { "version", no_argument, 0, 'V' },
@@ -208,6 +231,7 @@ static const struct option options[] = {
 #if HAVE_COREAUDIO_AUDIOHARDWARE_H
   { "core-audio", no_argument, 0, 'c' },
 #endif
+  { "dump", required_argument, 0, 'r' },
   { "socket", required_argument, 0, 's' },
   { "config", required_argument, 0, 'C' },
   { 0, 0, 0, 0 }
@@ -509,7 +533,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorder-playrtp version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
@@ -532,6 +556,7 @@ int main(int argc, char **argv) {
     struct sockaddr_in6 in6;
   };
   union any_sockaddr mgroup;
+  const char *dumpfile = 0;
 
   static const struct addrinfo prefs = {
     AI_PASSIVE,
@@ -546,7 +571,7 @@ int main(int argc, char **argv) {
 
   mem_init();
   if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
-  while((n = getopt_long(argc, argv, "hVdD:m:b:x:L:R:M:aocC:", options, 0)) >= 0) {
+  while((n = getopt_long(argc, argv, "hVdD:m:b:x:L:R:M:aocC:r", options, 0)) >= 0) {
     switch(n) {
     case 'h': help();
     case 'V': version();
@@ -568,6 +593,7 @@ int main(int argc, char **argv) {
 #endif
     case 'C': configfile = optarg; break;
     case 's': control_socket = optarg; break;
+    case 'r': dumpfile = optarg; break;
     default: fatal(0, "invalid option");
     }
   }
@@ -676,6 +702,27 @@ int main(int argc, char **argv) {
     if((err = pthread_create(&tid, 0, control_thread, 0)))
       fatal(err, "pthread_create control_thread");
   }
+  if(dumpfile) {
+    int fd;
+    unsigned char buffer[65536];
+    size_t written;
+
+    if((fd = open(dumpfile, O_RDWR|O_TRUNC|O_CREAT, 0666)) < 0)
+      fatal(errno, "opening %s", dumpfile);
+    /* Fill with 0s to a suitable size */
+    memset(buffer, 0, sizeof buffer);
+    for(written = 0; written < dump_size * sizeof(int16_t);
+        written += sizeof buffer) {
+      if(write(fd, buffer, sizeof buffer) < 0)
+        fatal(errno, "clearing %s", dumpfile);
+    }
+    /* Map the buffer into memory for convenience */
+    dump_buffer = mmap(0, dump_size * sizeof(int16_t), PROT_READ|PROT_WRITE,
+                       MAP_SHARED, fd, 0);
+    if(dump_buffer == (void *)-1)
+      fatal(errno, "mapping %s", dumpfile);
+    info("dumping to %s", dumpfile);
+  }
   play_rtp();
   return 0;
 }
index dd9fa5de4222705c5649c950ccd7a41e9135275a..565ca0b7d87bb5360c65a00c1cffc4ccc9dc983a 100644 (file)
@@ -144,6 +144,10 @@ extern pthread_mutex_t lock;
 extern pthread_cond_t cond;
 extern unsigned minbuffer;
 
+extern int16_t *dump_buffer;
+extern size_t dump_index;
+extern size_t dump_size;
+
 void playrtp_oss(void), playrtp_alsa(void), playrtp_coreaudio(void);
 
 #endif /* PLAYRTP_H */
index 99b2e9726b5028a11884a095af9a051bfa04edaa..9e353f8c0d4f97565a0b58619a589ed1bd1699da 100644 (file)
@@ -2,6 +2,7 @@
 #
 # This file is part of DisOrder.
 # Copyright (C) 2004, 2005, 2006, 2007 Richard Kettlewell
+# Portions copyright (C) 2007 Ross Younger
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -19,9 +20,9 @@
 # USA
 #
 
-AC_INIT([disorder], [1.5.99+], [richard+disorder@sfere.greenend.org.uk])
+AC_INIT([disorder], [2.0+], [richard+disorder@sfere.greenend.org.uk])
 AC_CONFIG_AUX_DIR([config.aux])
-AM_INIT_AUTOMAKE(disorder, [1.5.99+])
+AM_INIT_AUTOMAKE(disorder, [2.0+])
 AC_CONFIG_SRCDIR([server/disorderd.c])
 AM_CONFIG_HEADER([config.h])
 
@@ -466,10 +467,10 @@ if test "x$GCC" = xyes; then
   if test $rjk_cv_shadow = yes; then
     CC="${CC} -Wshadow"
   fi
-                  
-
 fi
 
+RJK_GCOV
+
 AH_BOTTOM([#ifdef __GNUC__
 # define attribute(x) __attribute__(x)
 #else
index f3bf2fd28ba88a19054357935beafbe78a0f1173..484da6d6466da2c667bcee871e060f44850e10f6 100644 (file)
@@ -1,3 +1,15 @@
+disorder (2.0) unstable; urgency=low
+
+  * DisOrder 2.0
+
+ -- Richard Kettlewell <rjk@greenend.org.uk>  Mon, 17 Dec 2007 18:17:55 +0000
+
+disorder (1.5.99+dev10) unstable; urgency=low
+
+  * Disobedience now pops up a login box on startup if not yet configured.
+
+ -- Richard Kettlewell <rjk@greenend.org.uk>  Sun,  9 Dec 2007 23:12:29 +0000
+
 disorder (1.5.99+dev9) unstable; urgency=low
 
   * disorder.unicode branch has been merged
diff --git a/debian/conffiles.disorder b/debian/conffiles.disorder
new file mode 100644 (file)
index 0000000..a9d34ea
--- /dev/null
@@ -0,0 +1 @@
+/etc/bash_completion.d/disorder
index a7e2c5875a2920d37c5ba06368835a70720a9e99..9eb4d1918dafdc95c44895468894d67a62ee1ad5 100755 (executable)
@@ -74,6 +74,7 @@ pkg-disorder: build
        $(MKDIR) debian/disorder
        $(MKDIR) debian/disorder/DEBIAN
        $(MKDIR) debian/disorder/usr/share/doc/disorder
+       $(MKDIR) debian/disorder/etc/bash_completion.d
        $(INSTALL_DATA) debian/copyright \
                debian/disorder/usr/share/doc/disorder/copyright
        $(INSTALL_DATA) debian/changelog \
@@ -88,9 +89,11 @@ pkg-disorder: build
        $(MAKE) DESTDIR=`pwd`/debian/disorder installdirs install -C doc
        $(MAKE) DESTDIR=`pwd`/debian/disorder installdirs install -C clients
        $(MAKE) DESTDIR=`pwd`/debian/disorder installdirs install -C lib
-       $(MAKE) DESTDIR=`pwd`/debian/disorder installdirs install -C scripts
+       $(INSTALL_DATA) scripts/completion.bash \
+               debian/disorder/etc/bash_completion.d/disorder
        rm -rf debian/disorder/usr/share/man/man8
        rm -rf debian/disorder/usr/share/disorder/*.html
+       rmdir debian/disorder/usr/share/disorder
        rm -f debian/disorder/usr/bin/disorder-playrtp
        rm -f debian/disorder/usr/bin/disobedience
        rm -f debian/disorder/usr/share/man/man1/disorder-playrtp.1
index 62535e17ec98fd1da494ea4ad3b489d47476532b..bcab45a90026b5c22abfc31a97bd9ddac8e9367f 100644 (file)
@@ -23,7 +23,7 @@ doc_DATA=disobedience.html
 
 AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
 AM_CFLAGS=$(GLIB_CFLAGS) $(GTK_CFLAGS)
-PNGS:=$(wildcard ${top_srcdir}/images/*.png)
+PNGS:=$(shell export LC_COLLATE=C;echo ${top_srcdir}/images/*.png)
 
 disobedience_SOURCES=disobedience.h disobedience.c client.c queue.c    \
                  choose.c misc.c control.c properties.c menu.c \
index d6b346d05cdf9c1672c035c1946d080ef800b599..de71101bb34ca3d6787ce9c9a9a0492e9c313b61 100644 (file)
@@ -409,7 +409,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorder version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
@@ -492,6 +492,9 @@ int main(int argc, char **argv) {
   /* See if RTP play supported */
   check_rtp_address();
   suppress_actions = 0;
+  /* If no password is set yet pop up a login box */
+  if(!config->password)
+    login_box();
   D(("enter main loop"));
   MTAG("misc");
   g_main_loop_run(mainloop);
index 216b5de90abd43b089c8a5ad0385f9aa2db5b31c..346e2fae5ceea66e8d32881d8e73b3cf622bbd29 100644 (file)
@@ -124,9 +124,12 @@ static void about_popup_got_version(void attribute((unused)) *v,
                                     const char *value) {
   GtkWidget *w;
   char *server_version_string;
+  char *short_version_string;
   GtkWidget *hbox, *vbox, *title;
 
   byte_xasprintf(&server_version_string, "Server version %s", value);
+  byte_xasprintf(&short_version_string, "Disobedience %s",
+                 disorder_short_version_string);
   w = gtk_dialog_new_with_buttons("About Disobedience",
                                   GTK_WINDOW(toplevel),
                                   (GTK_DIALOG_MODAL
@@ -142,7 +145,7 @@ static void about_popup_got_version(void attribute((unused)) *v,
                      FALSE/*fill*/,
                      4/*padding*/);
   gtk_box_pack_start(GTK_BOX(vbox),
-                     gtk_label_new("Disobedience " VERSION),
+                     gtk_label_new(short_version_string),
                      FALSE/*expand*/,
                      FALSE/*fill*/,
                      1/*padding*/);
@@ -152,7 +155,7 @@ static void about_popup_got_version(void attribute((unused)) *v,
                      FALSE/*fill*/,
                      1/*padding*/);
   gtk_box_pack_start(GTK_BOX(vbox),
-                     gtk_label_new("(c) 2004-2007 Richard Kettlewell"),
+                     gtk_label_new("\xC2\xA9 2004-2007 Richard Kettlewell"),
                      FALSE/*expand*/,
                      FALSE/*fill*/,
                      1/*padding*/);
index 9410f2a334576c71780906cbe2d8d61826f69215..cc177566fbaf799194c34ccd493983afaa14f2d3 100644 (file)
@@ -301,29 +301,6 @@ The screen number to use.
 .\" .TP
 .\" .B --sync
 .\" Make all X requests synchronously.
-.SH "GTK+ RESOURCES"
-You can override these resources in order to customize the appearance of
-Disobedience.
-.\" TODO example that actually works.
-.SS "Widget Names"
-.TP
-.B disobedience.*.choose
-This is the panel containing the track choice tree.
-.TP
-.B disobedience.*.queue
-This is the panel displaying the queue.
-.TP
-.B disobedience.*.choose
-This is the panel listing recently played tracks.
-.TP
-.B disobedience.*.row-playing
-This is the row listing the currently playing track.
-.TP
-.B disobedience.*.row-odd
-This an odd-numbered row in the queue or recently played track list.
-.TP
-.B disobedience.*.row-even
-This an even-numbered row in the queue or recently played track list.
 .SH CONFIGURATION
 If you are using
 .B disobedience
@@ -339,6 +316,13 @@ The other clients read their configuration from the same location so after
 setting up with Disobedience, tools such as
 .BR disorder (1)
 should work as well.
+.SH BUGS
+Disobedience is newly introduced with DisOrder 2.0.  There are bound to be
+bugs.  Please send feedback.
+.PP
+There is no particular provision for multiple users of the same computer
+sharing a single \fBdisorder-playrtp\fR process.  This shouldn't be too much of
+a problem in practice but something could perhaps be done given demand.
 .SH FILES
 .TP
 .I ~/.disorder/HOSTNAME-rtp
index 7da152b465bf84d4ab4909f6036066dd16a9ce58..3a6c2d70be55f6598f2928350736b03940ce308d 100644 (file)
@@ -26,7 +26,7 @@ disorder-dbupgrade \- DisOrder Database Upgrader
 .SH DESCRIPTION
 .B disorder-dbupgrade
 is DisOrder's database upgrader.  It is invoked by DisOrder when
-necessary and does not need to be invoked manually.
+necessary and does not normally need to be invoked manually.
 .SH OPTIONS
 .TP
 .B --delete-bad-keys\fR, -x
@@ -45,6 +45,12 @@ Set the configuration file.
 .B --debug\fR, \fB-d
 Enable debugging.
 .TP
+.B --syslog
+Log to syslog.  This is the default if stderr is not a terminal.
+.TP
+.B --no-syslog
+Do not log to syslog.  This is the default if stderr is a terminal.
+.TP
 .B --help\fR, \fB-h
 Display a usage message.
 .TP
@@ -56,7 +62,7 @@ to NFC for some reason.  By default a warning message is issued and
 they are left in the database (if doing so will not compromise its
 integrity).  The
 .B -x
-option can be used to delete them if they are known to be harmles.
+option can be used to delete them if they are known to be harmless.
 .SH "SEE ALSO"
 \fBdisorderd\fR(8), \fBdisorder_config\fR(5)
 .\" Local Variables:
index 8957186a50e2a6ee1d94c1be8364a5b45e6bdb3e..df6db6f10fc4e3ef434187be953cc8653035010d 100644 (file)
@@ -34,6 +34,12 @@ Set the configuration file.
 .B --debug\fR, \fB-d
 Enable debugging.
 .TP
+.B --syslog
+Log to syslog.  This is the default if stderr is not a terminal.
+.TP
+.B --no-syslog
+Do not log to syslog.  This is the default if stderr is a terminal.
+.TP
 .B --help\fR, \fB-h
 Display a usage message.
 .TP
index 91c59968a3833344f7243c38c2fc5195c4e9a06a..f4ae673a3829ff5d9ed407736c6f80cf5857bf75 100644 (file)
@@ -31,6 +31,13 @@ therefore suitable for use as an
 player.
 .PP
 It is not intended to be used from the command line.
+.SH OPTIONS
+.TP
+.B --help\fR, \fB-h
+Display a usage message.
+.TP
+.B --version\fR, \fB-V
+Display version number.
 .SH LIMITATIONS
 OGG files with multiple bitstreams are not supported.
 .PP
index c8767386c5ee05a5882f11aec6085aa84590764d..fe7e8b40ec35a8f9d3447011a927790cd205df59 100644 (file)
@@ -77,10 +77,6 @@ Taking a backup of the non-regeneratable parts of DisOrder's databases.
 .B .
 Indoctrinating one DisOrder server with the preference values of
 another.  
-.TP
-.B .
-Upgrading DisOrder across data format changes in the underlying
-database library.
 .PP
 The output file is versioned, so versions produced from a future
 version of DisOrder may be rejected by \fB--undump\fR.  It has an end
index b48b4af23fb5b5744d2dec9141e7418d3b78d6d4..7629439a2f00feecf9c06dd69f43a3bc300aa31b 100644 (file)
@@ -27,5 +27,24 @@ is used by
 .BR disorderd (8)
 to convert audio data to a consistent encoding.  It is not intended to
 be used by ordinary users.
+.SH OPTIONS
+.TP
+.B --config \fIPATH\fR, \fB-c \fIPATH
+Set the configuration file.
+.TP
+.B --debug\fR, \fB-d
+Enable debugging.
+.TP
+.B --syslog
+Log to syslog.  This is the default if stderr is not a terminal.
+.TP
+.B --no-syslog
+Do not log to syslog.  This is the default if stderr is a terminal.
+.TP
+.B --help\fR, \fB-h
+Display a usage message.
+.TP
+.B --version\fR, \fB-V
+Display version number.
 .SH "SEE ALSO"
 .BR disorderd (8)
index f4bcfd6f7cc9f8ec3b01d7f49f06dce6ef85e9f4..bdd05ec75dd486371215ac08ac8f13d0b1e3ec56 100644 (file)
@@ -39,11 +39,29 @@ broadcast to that port.
 If a group and a port are specified then the RTP stream is assumed to be
 multicast to that group and port.
 .SH OPTIONS
+The default sound API is the first of the ones listed below that are available.
+Usually this implies ALSA under Linux and Core Audio under OS X.
+.TP
+.B --alsa\fR, \fB-a
+Use ALSA to play sound.
+.TP
+.B --oss\fR, \fB-o
+Use OSS to play sound.
+.TP
+.B --core-audio\fR, \fB-c
+Use Core Audio to play sound.
 .TP
 .B --device \fIDEVICE\fR, \fB-D \fIDEVICE\fR
 Specifies the audio device to use.  The exact meaning of this is
 platform-dependent; on Linux it is the ALSA device name.
 .TP
+.B --config \fIPATH\fR, \fB-C \fIPATH
+Set the configuration file.  The default is
+.IR pkgconfdir/config .
+.TP
+.B --socket \fIPATH\fR, \fB-s \fIPATH
+Set the control socket.  Normally this would not be used manually.
+.TP
 .B --help\fR, \fB-h
 Display a usage message.
 .TP
@@ -68,7 +86,29 @@ is four times the \fB--buffer\fR value.
 .B --rcvbuf \fIBYTES\fR, \fB-R \fIBYTES\fR
 Specifies socket receive buffer size.  The default is 131072 (128Kbytes).  The
 buffer size will not be reduced below the operating system's default.
+.SH "REMOTE CONTROL"
+The
+.B --socket
+option is used by Disobedience to control a background
+.B disorder-playrtp
+daemon.  The socket will be created as a UNIX domain stream socket.  When a
+connection is received a single line is read from it.  The following commands
+are known:
+.TP
+.B stop
+Causes
+.B disorder-playrtp
+to terminate.
+.TP
+.B query
+Causes the string "running" to be sent back.
+.PP
+Other commands are ignored.  After the first command the connection is closed.
+Only one connection at a time will be serviced.
+.PP
+This protocol is not guaranteed to be stable.
 .SH "SEE ALSO"
+.BR disobedience (1),
 .BR disorder_config (5),
 .BR disorderd (8)
 .\" Local Variables:
index 94dfc2883b9c393f3a382f4d93cdbe56d21ffc61..6a204dee75944f52adb7c25b41cf0158b17295a8 100644 (file)
@@ -35,6 +35,12 @@ Set the configuration file.
 .B --debug\fR, \fB-d
 Enable debugging.
 .TP
+.B --syslog
+Log to syslog.  This is the default if stderr is not a terminal.
+.TP
+.B --no-syslog
+Do not log to syslog.  This is the default if stderr is a terminal.
+.TP
 .B --help\fR, \fB-h
 Display a usage message.
 .TP
index a304da4d6a572278b9b7d4acccd78d686baa82d3..b3b220835bb7746c96236deac81a5bf011809a87 100644 (file)
@@ -27,5 +27,24 @@ is used by
 .BR disorderd (8)
 to play digital audio with buffering and avoiding gaps between
 tracks.  It is not intended for direct invocation.
+.SH OPTIONS
+.TP
+.B --config \fIPATH\fR, \fB-c \fIPATH
+Set the configuration file.
+.TP
+.B --debug\fR, \fB-d
+Enable debugging.
+.TP
+.B --syslog
+Log to syslog.  This is the default if stderr is not a terminal.
+.TP
+.B --no-syslog
+Do not log to syslog.  This is the default if stderr is a terminal.
+.TP
+.B --help\fR, \fB-h
+Display a usage message.
+.TP
+.B --version\fR, \fB-V
+Display version number.
 .SH "SEE ALSO"
 .BR disorderd (8)
index 4ceda15fbbe836e47eb8178b5f9c1a96a2ccac6b..4719a3e3ef1fcda028f20f6c4d10688cd97fd6ac 100644 (file)
@@ -34,6 +34,12 @@ Set the configuration file.
 .B --debug\fR, \fB-d
 Enable debugging.
 .TP
+.B --syslog
+Log to syslog.  This is the default if stderr is not a terminal.
+.TP
+.B --no-syslog
+Do not log to syslog.  This is the default if stderr is a terminal.
+.TP
 .B --help\fR, \fB-h
 Display a usage message.
 .TP
index a05ca8c1b9b58637689507e75e144aa992f51412..89863dea7bedeebcb4a676d9b0073c1c442a465c 100644 (file)
@@ -24,14 +24,7 @@ disorder \- DisOrder jukebox client
 .RI [ OPTIONS ]
 .RB [ -- ]
 .RI [ COMMANDS ...]
-.br
-.B disorder
-.B --length
-.RI [ OPTIONS ]
-.RB [ -- ]
-.IR PATH ...
 .SH DESCRIPTION
-Without the \fB--length\fR option,
 .B disorder
 is used to query the \fBdisorderd\fR(8) daemon from the command line.
 It may be used to request tracks, scratch tracks, query the current
@@ -54,10 +47,6 @@ Enable debugging.
 .B --help\fR, \fB-h
 Display a usage message.
 .TP
-.B --length\fR, \fB-L
-Calculate the length in seconds of the files specified using the tracklength
-plugin.
-.TP
 .B --version\fR, \fB-V
 Display version number.
 .TP
index f84bd165cb9a4cabcf981af10f199141c97c1c33..b4d811ddedca852d1c5c7be7cbff517e7581f784 100644 (file)
@@ -77,6 +77,9 @@ you do not need to free the results.
 .IP
 Floating point conversions and wide character support are not
 currently implemented.
+.IP
+These functions will cope with UTF-8 even if the current locale uses
+some other encoding.
 .PP
 "Never fail" in the above means that the process is terminated on error.
 .SH LOGGING
@@ -167,8 +170,8 @@ Currently this means that they are not even defined outside the
 server.
 .PP
 All strings in this section are encoded using UTF-8.
-.SS tracklength.so
-This is a server plugin.
+.SS "Tracklength Plugins"
+These are server plugins defined by the \fBtracklength\fR directive.
 .PP
 .nf
 \fBlong disorder_tracklength(const char *track,
@@ -181,6 +184,9 @@ null pointer otherwise.  \fBpath\fR will be the same byte string return from
 the scanner plugin, and so presumably encoded according to the
 filesystem encoding.
 .IP
+To clarify this point, if the track must be opened to compute its
+length, you would normally use \fBpath\fR and not \fBtrack\fR.
+.IP
 If the return value is positive it should be the track length in
 seconds (round up if it is not an integral number of seconds long).
 .IP
index badedebca2b7b6355f125d6c450fd8f3432f064d..6cb4a7897ddc4168a8cc89d88b57b773c5295879 100644 (file)
@@ -72,11 +72,33 @@ override specific bits.
 The web interface connects to the DisOrder server like any other user, though
 it is given a special privilege to "become" any other user.  (Thus, any process
 with the same UID as the web interface is very powerful as far as DisOrder
-goes.)
+goes.  This model will be changed in a future version.)
 .PP
 Access control to the web interface is (currently) separate from DisOrder's own
 access control (HTTP authentication is required) but uses the same user
 namespace.
+.SS "Searching And Tags"
+Search strings contain a list of search terms separated by spaces.  A search
+term can either be a single word or a tag, prefixed with "tag:".
+.PP
+Search words are compared without regard to letter case or accents; thus, all
+of the following will be considered to be equal to one another:
+.PP
+.nf
+  LATIN CAPITAL LETTER E
+  LATIN SMALL LETTER E
+  LATIN CAPITAL LETTER E WITH GRAVE
+  LATIN SMALL LETTER E WITH GRAVE
+  LATIN CAPITAL LETTER E plus COMBINING GRAVE ACCENT
+  LATIN SMALL LETTER E plus COMBINING GRAVE ACCENT
+.fi
+.PP
+The same rules apply to tags but in addition leading and trailing whitespace is
+disregarded and all whitespace sequences are treated as equal when they appear
+as internal whitespace.
+.PP
+Where several tags are listed, for instance the tags preference for a track,
+the tags are separated by commas.  Therefore tags may not contain commas.
 .SH "CONFIGURATION FILE"
 .SS "General Syntax"
 Lines are split into fields separated by whitespace (space, tab, line
@@ -148,7 +170,7 @@ automatically included, but should include the proper extension.
 .IP
 The default is \fB{/artist}{/album}{/title}{ext}\fR.
 .TP
-.B authorization_algorthm \fIALGORITHM\fR
+.B authorization_algorithm \fIALGORITHM\fR
 Defines the algorithm used to authenticate clients.  The valid options
 are sha1 (the default), sha256, sha384 and sha512.  See
 .BR disorder_protocol (5)
@@ -157,6 +179,8 @@ for more details.
 .B broadcast \fIADDRESS\fR \fIPORT\fR
 Transmit sound data to \fIADDRESS\fR using UDP port \fIPORT\fR.  This implies
 \fBspeaker_backend network\fR.
+.IP
+See also \fBmulticast_loop\fR and \fBmulticast_ttl\fR.
 .TP
 .B broadcast_from \fIADDRESS\fR \fIPORT\fR
 Sets the (local) source address used by \fBbroadcast\fR.
@@ -168,7 +192,8 @@ are:
 .RS
 .TP 8
 .B pcm
-Output level for the audio device.  This is probably what you want.
+Output level for the audio device.  This is probably what you want and is the
+default.
 .TP
 .B speaker
 Output level for the PC speaker, if that is connected to the sound card.
@@ -181,7 +206,8 @@ Master output level.  The OSS documentation recommends against using this, as
 it affects all output devices.
 .RE
 .IP
-You can also specify channels by number, if you know the right value.
+You can also specify channels by number, if you know the right value.  NB that
+volume setting only works on OSS systems (including ALSA, via emulation).
 .TP
 .B collection \fIMODULE\fR \fIENCODING\fR \fIROOT\fR
 Define a collection of tracks.
@@ -218,17 +244,24 @@ Normally the server only listens on a UNIX domain socket.
 .TP
 .B lock yes\fR|\fBno
 Determines whether the server locks against concurrent operation.  Default is
-\fByes\fR.
+\fByes\fR.  There is no good reason to set this to \fBno\fR and the option will
+probably be removed in a future version.
 .TP
 .B mixer \fIPATH\fR
 The path to the mixer device, if you want access to the volume control,
-e.g. \fB/dev/mixer\fR.
+e.g. \fB/dev/mixer\fR (the default).
 .TP
-.B multicast_ttl \fIHOPS\fR
-Set the maximum number of hops to send multicast packets.  This only applies is
+.B multicast_loop yes\fR|\fBno
+Determines whether multicast packets are loop backed to the sending host.  The
+default is \fByes\fR.  This only applies if
 \fBspeaker_backend\fR is set to \fBnetwork\fR and \fBbroadcast\fR is actually a
 multicast address.
 .TP
+.B multicast_ttl \fIHOPS\fR
+Set the maximum number of hops to send multicast packets.  This only applies if
+\fBspeaker_backend\fR is set to \fBnetwork\fR and \fBbroadcast\fR is actually a
+multicast address.  The default is 1.
+.TP
 .B namepart \fIPART\fR \fIREGEXP\fR \fISUBST\fR [\fICONTEXT\fR [\fIREFLAGS\fR]]
 Determines how to extract trackname part \fIPART\fR from a 
 track name (with the collection root part removed).
@@ -253,7 +286,15 @@ that aren't in the original track name will lead to confusing results.
 .IP
 If you supply no \fBnamepart\fR directives at all then a default set will be
 supplied automatically.  But if you supply even one then you must supply all of
-them.  See the example config file for the defaults.
+them.  The defaults are equivalent to:
+.PP
+.nf
+namepart title  "/([0-9]+ *[-:] *)?([^/]+)\\.[a-zA-Z0-9]+$" $2 display
+namepart title  "/([^/]+)\\.[a-zA-Z0-9]+$"                  $1 sort
+namepart album  "/([^/]+)/[^/]+$"                          $1 *
+namepart artist "/([^/]+)/[^/]+/[^/]+$"                    $1 *
+namepart ext    "(\\.[a-zA-Z0-9]+)$"                        $1 *
+.fi
 .TP
 .B nice_rescan \fIPRIORITY\fR
 Set the recan subprocess priority.  The default is 10.
@@ -341,7 +382,7 @@ to 3600, i.e. one hour.
 .TP
 .B queue_pad \fICOUNT\fR
 The target size of the queue.  If random play is enabled then randomly picked
-tracks will be added until the queue is at least this big.
+tracks will be added until the queue is at least this big.  The default is 10.
 .TP
 .B restrict \fR[\fBscratch\fR] [\fBremove\fR] [\fBmove\fR]
 Determine which operations are restricted to the submitter of a
@@ -408,8 +449,7 @@ Use Apple Core Audio.  This only available on OS X systems, on which it is the
 default.
 .TP
 .B oss
-Use the OSS (/dev/dsp) API.  Not available on all platforms.  Not well
-maintained at the moment.
+Use the OSS (/dev/dsp) API.  Not available on all platforms.
 .TP
 .B command
 Execute a command.  This is the default if
@@ -425,8 +465,9 @@ to receive and play the resulting stream on Linux and OS X.
 .TP
 .B sox_generation \fB0\fR|\fB1
 Determines whether calls to \fBsox\fR(1) should use \fB-b\fR, \fB-x\fR, etc (if
-the generation is 0) or \fB-\fIbits\fR, \fB-L\fR etc (if it is 1).  The default
-is 0.
+the generation is 0) or \fB-\fIbits\fR, \fB-L\fR etc (if it is 1).  See the
+documentation for your installed copy of \fBsox\fR to determine which you need.
+The default is 0.
 .TP
 .B speaker_command \fICOMMAND
 Causes the speaker subprocess to pipe audio data into shell command
@@ -507,7 +548,15 @@ match then each is executed in order.
 .IP
 If you supply no \fBtransform\fR directives at all then a default set will be
 supplied automatically.  But if you supply even one then you must supply all of
-them.  See the example config file for the defaults.
+them.  The defaults are:
+.PP
+.nf
+transform track "^.*/([0-9]+ *[-:] *)?([^/]+)\\.[a-zA-Z0-9]+$" $2 display
+transform track "^.*/([^/]+)\\.[a-zA-Z0-9]+$"        $1 sort
+transform dir   "^.*/([^/]+)$"                      $1 *
+transform dir   "^(the) ([^/]*)"                    "$2 $1" sort i
+transform dir   "[[:punct:]]"                       "" sort g
+.fi
 .TP
 .B url \fIURL\fR
 Specifies the URL of the web interface.  This URL will be used in
@@ -568,10 +617,6 @@ These are the values set with \fBset-global\fR.
 .B required-tags
 If this is set an nonempty then randomly played tracks will always have at
 least one of the listed tags.
-.IP
-Tags can contain any printing character except comma.  Leading and trailing
-spaces are not significant but internal spaces are.  Tags in a list are
-separated by commas.
 .TP
 .B prohibited-tags
 If this is set an nonempty then randomly played tracks will never have any of
@@ -731,7 +776,7 @@ If there are no arguments, or all the arguments are \fBtrue\fB, then expands to
 \fBtrue\fR, otherwise to \fBfalse\fR.
 .TP
 .B @arg:\fINAME\fB@
-Expands to the value of CGI script argument \fINAME\fR.
+Expands to the value of CGI argument \fINAME\fR.
 .TP
 .B @basename@
 The basename of the current directory component, in \fB@navigate@\fR.
index bf293e91b09461ada7c16297acd2fcdbbd78c510..a9d973d1693639facb3f1af023524c0c25204a53 100644 (file)
@@ -83,9 +83,13 @@ If \fIREGEXP\fR is present only matching files are returned.
 .B get \fITRACK\fR \fIPREF\fR
 Gets a preference value.  On success the second field of the response line will
 have the value.
+.IP
+If the track or preference do not exist then the response code is 555.
 .TP
 .B get-global \fIKEY\fR
 Get a global preference.
+.IP
+If the preference does not exist then the response code is 555.
 .TP
 .B length \fITRACK\fR
 Gets the length of the track in seconds.  On success the second field of the
@@ -141,7 +145,7 @@ or
 Pause the current track.
 .TP
 .B play \fITRACK\fR
-Add a track to the queue.
+Add a track to the queue.  The response contains the queue ID of the track.
 .TP
 .B playing
 Reports what track is playing.
@@ -293,6 +297,10 @@ Text part is just commentary; a dot-stuffed body follows.
 Text part is just commentary; an indefinite dot-stuffed body follows.  (Used
 for \fBlog\fR.)
 .TP
+.B 5
+Used with "normal" errors, for instance a preference not being found.  The text
+part is commentary.
+.TP
 .B 9
 The text part is just commentary (but would normally be a response for this
 command) e.g. \fBplaying\fR.
index 24cf033d36e84d00da6c8d8eec3eee607707ff60..71b9568a1d10b45fb3e8377ceacd222ad389ad32 100644 (file)
@@ -38,11 +38,14 @@ for further information.
 .B --pidfile \fIPATH\fR, \fB-P \fIPATH
 Write a pidfile.
 .TP
-.B --foreground, \fB-f
+.B --foreground\fR, \fB-f
 Run in the foreground.  (By default,
 .B disorderd
 detaches from its terminal and runs in the background.)
 .TP
+.B --syslog\fR, \fB-s
+Log to syslog.  This is the default if DisOrder runs in the background.
+.TP
 .B --debug\fR, \fB-d
 Enable debugging.
 .TP
@@ -52,14 +55,16 @@ Display a usage message.
 .B --version\fR, \fB-V
 Display version number.
 .SH NOTES
-.SS "Environmental Dependencies"
-It is important that
-.B disorder-deadlock
-and
+For configuration file documentation, see
+.BR disorder_config (5).
+.SS "Startup"
+The first time a new install of DisOrder is started it will run
 .B disorder-rescan
-are available on the PATH.  The example "init" script attempts to
-ensure this by appending sbindir to the path, but if you have
-installed programs in unusual locations then this might not work.
+to pick up new tracks.  On subsequent server restarts it will NOT do
+this automatically; if you want a rescan at every restart you must
+arrange that manually.
+.PP
+There is however an automatic rescan once every 24 hours.
 .SS "How To Configure Authentication"
 The administrator should create \fIpkgconfdir/config.private\fR, make sure it
 is not world-readable, and populate it with \fBallow\fR commands
@@ -78,10 +83,8 @@ is locale-aware.  If you do not set the locale correctly then it may
 not handle non-ASCII data properly.
 .PP
 Filenames and the configuration file are assumed to be encoded using the
-current locale.  The
-communication with the client happens in UTF-8 (since the client and the server
-don't know what locale each other might be using - in the future they might not
-even be on the same host.)
+current locale.  Internally (within the server, in the database and in
+communication between client and server) the UTF-8 encoding is used.
 .SS Backups
 DisOrder uses Berkeley DB but currently discards log files that are no longer
 in use.  This means that DB's catastrophic recovery cannot be used (normal
@@ -126,8 +129,14 @@ Do not edit while the daemon is running.
 .I pkgstatedir/prefs.db
 Preferences database.
 .TP
+.I pkgstatedir/global.db
+Global preferences database.
+.TP
 .I pkgstatedir/search.db
-Search database.
+Search lookup database.
+.TP
+.I pkgstatedir/tags.db
+Tag lookup database.
 .TP
 .I pkgstatedir/tracks.db
 Tracks database.
@@ -137,8 +146,8 @@ Berkeley DB configuration file.  This may be used to override database
 settings without recompiling DisOrder.  See the Berkeley DB
 documention for further details.
 .TP
-.I pkgstatedir/log.*
-Database log files.
+.I pkgstatedir/log.* \fRand \fIpkgstatedir/__db.*
+Database internal files.
 .TP
 .I pkgstatedir/socket
 Communication socket for \fBdisorder\fR(1).
@@ -146,12 +155,6 @@ Communication socket for \fBdisorder\fR(1).
 .I pkgstatedir/lock
 Lockfile.  This prevents multiple instances of DisOrder running
 simultaneously.
-.TP
-.I sbindir/disorder-deadlock
-Deadlock manager.
-.TP
-.I sbindir/disorder-rescan
-Rescanner.
 .SH ENVIRONMENT
 .TP
 .B LC_ALL\fR, \fBLANG\fR, etc
index b6f7e3b248c3fdfab39a2a5a4a586681cad433f2..233d506f321de5c602add81bb3ccd6d917c04186 100644 (file)
@@ -31,6 +31,8 @@ recursively links or copies files from
 to
 .IR DESTINATION ,
 transforming filenames along the way.
+.PP
+This program is not well-tested!
 .SH OPTIONS
 .SS "Filename Format"
 .TP
index 6d535be8a89f09e789f598f1a2634bd076405ce0..d32c6eca62ee3cad97707e931aa74642eed2e732 100644 (file)
@@ -24,7 +24,8 @@ tkdisorder \- DisOrder jukebox client
 .RI [ OPTIONS ]
 .SH DESCRIPTION
 .B tkdisorder
-is a simple graphical client for DisOrder.  It is not really finished.
+is a simple graphical client for DisOrder.  It is not finished and no further
+development is planned.  Use \fBdisobedience\fR(1) instead.
 .PP
 The main window is divided into two.  The top half contains the name
 of the current track and a progress bar indicating how far through
@@ -50,7 +51,7 @@ Display a usage message.
 .B --version\fR, \fB-V
 Display version number.
 .SH "SEE ALSO"
-\fBdisorder\fR(1), \fBdisorder_config\fR(5)
+\fBdisorder\fR(1), \fBdisobedience\fR(1), \fBdisorder_config\fR(5)
 .PP
 "\fBpydoc disorder\fR" for the Python API documentation.
 .\" Local Variables:
index b642c895db345eb65b1ee181089a6645e1562a53..cf538787ad21ca437069236eab36282d1ef40077 100644 (file)
@@ -65,13 +65,25 @@ libdisorder_a_SOURCES=charset.c charset.h           \
        user.h user.c                                   \
        unicode.h unicode.c                             \
        unidata.h unidata.c                             \
-       utf8.h utf8.c                                   \
        vacopy.h                                        \
        vector.c vector.h                               \
        wav.h wav.c                                     \
        wstat.c wstat.h                                 \
        disorder.h
 
+version-string: ../config.status ${top_srcdir}/scripts/make-version-string
+       CC="${CC}" ${top_srcdir}/scripts/make-version-string > $@.new
+       @if cmp $@.new $@; then \
+               echo rm -f $@.new; rm -f $@.new; else \
+               echo mv $@.new $@; mv $@.new $@; fi
+
+version.h: version-string ${top_srcdir}/scripts/text2c
+       ${top_srcdir}/scripts/text2c -extern disorder_version_string \
+               version-string > $@.new
+       @if cmp $@.new $@; then \
+               echo rm -f $@.new; rm -f $@.new; else \
+               echo mv $@.new $@; mv $@.new $@; fi
+
 definitions.h: Makefile
        rm -f $@.new
        echo "#define PKGLIBDIR \"${pkglibdir}\"" > $@.new
@@ -85,16 +97,22 @@ definitions.h: Makefile
        @if cmp $@.new $@; then \
                echo rm -f $@.new; rm -f $@.new; else \
                echo mv $@.new $@; mv $@.new $@; fi
-defs.o: definitions.h
-defs.lo: definitions.h
+defs.o: definitions.h version.h
+defs.lo: definitions.h version.h
 
-test_SOURCES=test.c
-test_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV)
+test_SOURCES=test.c memgc.c
+test_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
 test_DEPENDENCIES=libdisorder.a
 
 check: test #test.i
        ./test
 
+check-report: before-check check make-coverage-reports
+before-check:
+       rm -f *.gcda *.gcov
+make-coverage-reports:
+       ${GCOV} *.c | ${PYTHON} ../scripts/format-gcov-report --html . *.c
+
 rebuild-unicode:
        cd ${srcdir} && ${top_srcdir}/scripts/make-unidata
 
index 0b5b7837b9e9c3b4f13e9f375938281c0ef5c663..8288e4ec01ebf6f66254356075858c62fddc77df 100644 (file)
@@ -17,7 +17,8 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
-/** @file lib/addr.c Socket address support */
+/** @file lib/addr.c
+ * @brief Socket address support */
 
 #include <config.h>
 #include "types.h"
index fb1dd7f53f63edd90360b8361e744ee220a20f18..8bd703f033b8f2135a2b409d0ed033cc432bc28b 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2004 Richard Kettlewell
+ * Copyright (C) 2004, 2007 Richard Kettlewell
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,6 +17,8 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/addr.h
+ * @brief Socket address support */
 
 #ifndef ADDR_H
 #define ADDR_H
index c5644b41fbe5d01e3e169d65aef404ddce89185b..7b66725f2bef5cf55ae0228c1405a324c7a005db 100644 (file)
@@ -72,7 +72,7 @@ static unsigned divide(unsigned long *v, int nwords, unsigned long m) {
 }
 
 /** @brief Convert v to a chosen base
- * @param v Pointer to bigendian bignum
+ * @param v Pointer to bigendian bignum (modified!)
  * @param nwords Length of bignum
  * @param buffer Output buffer
  * @param bufsize Size of output buffer
@@ -91,7 +91,8 @@ int basen(unsigned long *v,
   static const char chars[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
   
   do {
-    if(i <= 1) return -1;      /* overflow */
+    if(i <= 1)
+      return -1;                       /* overflow */
     buffer[--i] = chars[divide(v, nwords, base)];
   } while(!zero(v, nwords));
   memmove(buffer, buffer + i, bufsize - i);
index d758751d3d061d1ac19aef263abcf67e865a0231..2dcf8f97e90f9712b80ac660cd9950b6791fe30d 100644 (file)
@@ -1,6 +1,7 @@
 /*
  * This file is part of DisOrder.
  * Copyright (C) 2004, 2005, 2006, 2007 Richard Kettlewell
+ * Portions copyright (C) 2007 Mark Wooding
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  */
 char *configfile;
 
+/** @brief Read user configuration
+ *
+ * If clear, the user-specific configuration is not read.
+ */
+int config_per_user = 1;
+
 /** @brief Config file parser state */
 struct config_state {
   /** @brief Filename */
@@ -900,6 +907,7 @@ static const struct conf conf[] = {
   { C(listen),           &type_stringlist,       validate_port },
   { C(lock),             &type_boolean,          validate_any },
   { C(mixer),            &type_string,           validate_ischr },
+  { C(multicast_loop),   &type_boolean,          validate_any },
   { C(multicast_ttl),    &type_integer,          validate_non_negative },
   { C(namepart),         &type_namepart,         validate_any },
   { C2(nice, nice_rescan), &type_integer,        validate_non_negative },
@@ -1043,6 +1051,7 @@ static struct config *config_default(void) {
   c->queue_pad = 10;
   c->speaker_backend = -1;
   c->multicast_ttl = 1;
+  c->multicast_loop = 1;
   c->authorization_algorithm = xstrdup("sha1");
   c->noticed_history = 31;
   c->short_display = 32;
@@ -1178,19 +1187,21 @@ int config_read(int server) {
     return -1;
   xfree(privconf);
   /* if there's a per-user system config file for this user, read it */
-  if(!(pw = getpwuid(getuid())))
-    fatal(0, "cannot determine our username");
-  if((privconf = config_usersysconf(pw))
-     && access(privconf, F_OK) == 0
-     && config_include(c, privconf))
+  if(config_per_user) {
+    if(!(pw = getpwuid(getuid())))
+      fatal(0, "cannot determine our username");
+    if((privconf = config_usersysconf(pw))
+       && access(privconf, F_OK) == 0
+       && config_include(c, privconf))
       return -1;
-  xfree(privconf);
-  /* if we have a password file, read it */
-  if((privconf = config_userconf(getenv("HOME"), pw))
-     && access(privconf, F_OK) == 0
-     && config_include(c, privconf))
-    return -1;
-  xfree(privconf);
+    xfree(privconf);
+    /* if we have a password file, read it */
+    if((privconf = config_userconf(getenv("HOME"), pw))
+       && access(privconf, F_OK) == 0
+       && config_include(c, privconf))
+      return -1;
+    xfree(privconf);
+  }
   /* install default namepart and transform settings */
   config_postdefaults(c, server);
   /* everything is good so we shall use the new config */
index a4ffa6337bfa047c828d8e6c111bd34465cd2228..02c953a83fca8e8a7bc99a701b41074440fa76f9 100644 (file)
@@ -243,6 +243,9 @@ struct config {
   /** @brief TTL for multicast packets */
   long multicast_ttl;
 
+  /** @brief Whether to loop back multicast packets */
+  int multicast_loop;
+  
   /* derived values: */
   int nparts;                          /* number of distinct name parts */
   char **parts;                                /* name part list  */
@@ -274,6 +277,7 @@ char *config_private(void);
 /* get the private config file */
 
 extern char *configfile;
+extern int config_per_user;
 
 #endif /* CONFIGURATION_H */
 
index c9668c05e4edbb53633a1ec81df89adb774b9ad4..19987cb675f9c08ad68b71befb422bee187ed223 100644 (file)
@@ -30,7 +30,7 @@
 #include "definitions.h"
 
 /** @brief Software version number */
-const char disorder_version_string[] = VERSION;
+const char disorder_short_version_string[] = VERSION;
 
 /** @brief Package library directory */
 const char pkglibdir[] = PKGLIBDIR;
@@ -59,6 +59,8 @@ const char finkbindir[] = FINKBINDIR;
 /** @brief Package documentation directory */
 const char docdir[] = DOCDIR;
 
+#include "version.h"
+
 /*
 Local Variables:
 c-basic-offset:2
index c340e8eca3a58cb9dce601fc37b33a176c00c339..dd75209c7813c27fe875676e69e4eb280b4a0bd2 100644 (file)
@@ -21,6 +21,7 @@
 #ifndef DEFS_H
 #define DEFS_H
 
+extern const char disorder_short_version_string[];
 extern const char disorder_version_string[];
 extern const char pkglibdir[];
 extern const char pkgconfdir[];
index d93412aeefd6ce2f851f655bff68b31871c1c342..2bf86e9dff3192863aa72212c2dced66b7e1c733 100644 (file)
@@ -236,10 +236,6 @@ disorder_eclient *disorder_eclient_new(const disorder_eclient_callbacks *cb,
   vector_init(&c->vec);
   dynstr_init(&c->input);
   dynstr_init(&c->output);
-  if(!config->password) {
-    error(0, "no password set");
-    return 0;
-  }
   return c;
 }
 
@@ -340,6 +336,11 @@ void disorder_eclient_polled(disorder_eclient *c, unsigned mode) {
 
   if(c->state == state_disconnected) {
     D(("state_disconnected"));
+    /* If there is no password yet then we cannot connect */
+    if(!config->password) {
+      comms_error(c, "no password is configured");
+      return;
+    }
     with_sockaddr(c, start_connect);
     /* might now be state_disconnected (on error), state_connecting (slow
      * connect) or state_connected (fast connect).  If state_disconnected then
index c385ef256eb2c59c013bedd484f6207e967caa91..c080b4d033beb914f22a433e177b05d0b4aa774a 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2005 Richard Kettlewell
+ * Copyright (C) 2005, 2007 Richard Kettlewell
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,6 +17,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/filepart.c
+ * @brief Filename parsing
+ */
 
 #include <config.h>
 #include "types.h"
 #include "filepart.h"
 #include "mem.h"
 
+/** @brief Return the directory part of @p path
+ * @param path Path to parse
+ * @return Directory part of @p path
+ *
+ * Extracts the directory part of @p path.  This is a simple lexical
+ * transformation and no canonicalization is performed.  The result will only
+ * ever end "/" if it is the root directory.
+ */
 char *d_dirname(const char *path) {
   const char *s;
 
   if((s = strrchr(path, '/'))) {
+    while(s > path && s[-1] == '/')
+      --s;
     if(s == path)
       return xstrdup("/");
     else
@@ -38,6 +51,16 @@ char *d_dirname(const char *path) {
     return xstrdup(".");
 }
 
+/** @brief Find the extension part of @p path
+ * @param path Path to parse
+ * @return Start of extension in @p path, or NULL
+ *
+ * The return value points into @p path and points at the "." at the start of
+ * the path.  If the basename has no extension the result is NULL.  Extensions
+ * are assumed to only contain the ASCII digits and letters.
+ *
+ * See also extension().
+ */
 static const char *find_extension(const char *path) {
   const char *q = path + strlen(path);
   
@@ -48,12 +71,29 @@ static const char *find_extension(const char *path) {
   return *q == '.' ? q : 0;
 }
 
+/** @brief Strip the extension from @p path
+ * @param path Path to parse
+ * @return @p path with extension removed, or @p path
+ *
+ * The extension is defined exactly as for find_extension().  The result might
+ * or might not point into @p path.
+ */
 const char *strip_extension(const char *path) {
   const char *q = find_extension(path);
 
   return q ? xstrndup(path, q - path) : path;
 }
 
+/** @brief Find the extension part of @p path
+ * @param path Path to parse
+ * @return Start of extension in @p path, or ""
+ *
+ * The return value may points into @p path and if so points at the "." at the
+ * start of the path.  If the basename has no extension the result is "".
+ * Extensions are assumed to only contain the ASCII digits and letters.
+ *
+ * See also find_extension().
+ */
 const char *extension(const char *path) {
   const char *q = find_extension(path);
 
index 446e6847bb10458650682ee8aedf7a8a2c015b0b..fdc90745064eceacb06174c68fc9136315782e2d 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2005 Richard Kettlewell
+ * Copyright (C) 2005, 2007 Richard Kettlewell
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,6 +17,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/filepart.h
+ * @brief Filename parsing
+ */
 
 #ifndef FILEPART_H
 #define FILEPART_H
index c8ffe3192e922f634fb4a9ad41e55e84b9f99175..5562c7e5b8976d805cc76c5265335b2e42cf1d6a 100644 (file)
@@ -27,6 +27,8 @@
 #include <string.h>
 #include <ctype.h>
 
+#include <stdio.h>
+
 #include "mem.h"
 #include "mime.h"
 #include "vector.h"
@@ -295,7 +297,7 @@ static int isboundary(const char *ptr, const char *boundary, size_t bl) {
          && (iscrlf(ptr + bl + 2)
              || (ptr[bl + 2] == '-'
                  && ptr[bl + 3] == '-'
-                 && iscrlf(ptr + bl + 4))));
+                 && (iscrlf(ptr + bl + 4) || *(ptr + bl + 4) == 0))));
 }
 
 static int isfinal(const char *ptr, const char *boundary, size_t bl) {
@@ -304,7 +306,7 @@ static int isfinal(const char *ptr, const char *boundary, size_t bl) {
          && !strncmp(ptr + 2, boundary, bl)
          && ptr[bl + 2] == '-'
          && ptr[bl + 3] == '-'
-         && iscrlf(ptr + bl + 4));
+         && (iscrlf(ptr + bl + 4) || *(ptr + bl + 4) == 0));
 }
 
 /** @brief Parse a multipart MIME body
@@ -322,12 +324,16 @@ int mime_multipart(const char *s,
   const char *start, *e;
   int ret;
 
-  if(!isboundary(s, boundary, bl)) return -1;
+  /* We must start with a boundary string */
+  if(!isboundary(s, boundary, bl))
+    return -1;
+  /* Keep going until we hit a final boundary */
   while(!isfinal(s, boundary, bl)) {
     s = strstr(s, "\r\n") + 2;
     start = s;
     while(!isboundary(s, boundary, bl)) {
-      if(!(e = strstr(s, "\r\n"))) return -1;
+      if(!(e = strstr(s, "\r\n")))
+       return -1;
       s = e + 2;
     }
     if((ret = callback(xstrndup(start,
index d52a1cc7fa595e8d6f860ccaa0ec5f0924534ba1..15478ce0a71c53b4277d3310d6a09b71382c8ac8 100644 (file)
@@ -284,11 +284,11 @@ static int output_integer(struct state *s, struct conversion *c) {
    * '-' beats '0'.
    */
   if(c->flags & f_left) {
-    if(pad && do_pad(s, ' ', pad) < 0) return -1;
     if(sign && do_write(s, &sign, 1)) return -1;
     if(xform && do_write(s, c->specifier->xform, xform)) return -1;
     if(prec && do_pad(s, '0', prec) < 0) return -1;
     if(ndigits && do_write(s, digits + dp, ndigits)) return -1;
+    if(pad && do_pad(s, ' ', pad) < 0) return -1;
   } else if(c->flags & f_zero) {
     if(sign && do_write(s, &sign, 1)) return -1;
     if(xform && do_write(s, c->specifier->xform, xform)) return -1;
@@ -296,11 +296,11 @@ static int output_integer(struct state *s, struct conversion *c) {
     if(prec && do_pad(s, '0', prec) < 0) return -1;
     if(ndigits && do_write(s, digits + dp, ndigits)) return -1;
   } else {
+    if(pad && do_pad(s, ' ', pad) < 0) return -1;
     if(sign && do_write(s, &sign, 1)) return -1;
     if(xform && do_write(s, c->specifier->xform, xform)) return -1;
     if(prec && do_pad(s, '0', prec) < 0) return -1;
     if(ndigits && do_write(s, digits + dp, ndigits)) return -1;
-    if(pad && do_pad(s, ' ', pad) < 0) return -1;
   }
   return 0;
 }
@@ -323,11 +323,11 @@ static int output_string(struct state *s, struct conversion *c) {
   } else
     pad = 0;
   if(c->flags & f_left) {
-    if(pad && do_pad(s, ' ', pad) < 0) return -1;
     if(do_write(s, str, len) < 0) return -1;
+    if(pad && do_pad(s, ' ', pad) < 0) return -1;
   } else {
-    if(do_write(s, str, len) < 0) return -1;
     if(pad && do_pad(s, ' ', pad) < 0) return -1;
+    if(do_write(s, str, len) < 0) return -1;
   }
   return 0;
   
@@ -344,11 +344,11 @@ static int output_char(struct state *s, struct conversion *c) {
   } else
     pad = 0;
   if(c->flags & f_left) {
-    if(pad && do_pad(s, ' ', pad) < 0) return -1;
     if(do_write(s, &ch, 1) < 0) return -1;
+    if(pad && do_pad(s, ' ', pad) < 0) return -1;
   } else {
-    if(do_write(s, &ch, 1) < 0) return -1;
     if(pad && do_pad(s, ' ', pad) < 0) return -1;
+    if(do_write(s, &ch, 1) < 0) return -1;
   }
   return 0;
 }
@@ -415,7 +415,7 @@ static int parse_conversion(struct conversion *c, const char *ptr) {
       ++ptr;
       c->precision = -1;
     } else
-      return -1;
+      c->precision = 0;
     c->flags |= f_precision;
   }
   /* length modifier */
index 3ae4c2650962533d94821e3de8edaa180e82ceaa..ce43fc583d1351fe57694cdc52b9934acc1366c5 100644 (file)
@@ -54,6 +54,7 @@ struct queue_entry {
   const struct plugin *pl;             /* plugin that's playing this track */
   void *data;                          /* player data */
   long sofar;                          /* how much played so far */
+  int prepared;                                /* true when connected to speaker */
   /* For DISORDER_PLAYER_PAUSES only: */
   time_t lastpaused, lastresumed;      /* when last paused/resumed, or 0 */
   long uptopause;                      /* how much played up to last pause */
index a7db75f41fc20d736d6e557b30c18ae32ec067dd..58ff5d1fa37bca9c8785a7334c4eca9fe303acb7 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2006 Richard Kettlewell
+ * Copyright (C) 2006, 2007 Richard Kettlewell
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,6 +17,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/selection.c
+ * @brief Select management for Disobedience
+ */
 
 #include <config.h>
 #include "types.h"
 #include "hash.h"
 #include "selection.h"
 
+/** @brief Create a new selection manager
+ * @return Pointer to @ref hash used to manage the selection
+ */
 hash *selection_new(void) {
   return hash_new(sizeof (int));
 }
 
+/** @brief Add or remove a key in a selection
+ * @param h Hash representing selection
+ * @param key Key to insert
+ * @param selected non-0 if key is selected, 0 if it is not
+ *
+ * @p key is copied so the pointer need not remain valid.  Newly selected keys
+ * are not marked as live.
+ */
 void selection_set(hash *h, const char *key, int selected) {
-  if(selected)
-    hash_add(h, key, xmalloc_noptr(sizeof (int)), HASH_INSERT_OR_REPLACE);
-  else
+  if(selected) {
+    int *const liveness = xmalloc_noptr(sizeof (int));
+    *liveness = 0;
+    hash_add(h, key, liveness, HASH_INSERT_OR_REPLACE);
+  } else
     hash_remove(h, key);
 }
 
+/** @brief Test whether a key is set in a selection
+ * @param h Hash representing selection
+ * @param key Key to check
+ * @return non-0 if key is present, 0 if it is not
+ */
 int selection_selected(hash *h, const char *key) {
   return hash_find(h, key) != 0;
 }
 
+/** @brief Invert a key's selection status
+ * @param h Hash representing selection
+ * @param key Key to flip
+ *
+ * If the key is selected as a result it is not marked as live.
+ */
 void selection_flip(hash *h, const char *key) {
   selection_set(h, key, !selection_selected(h, key));
 }
 
+/** @brief Mark a selection key as live
+ * @param h Hash representing selection
+ * @param key Key to mark as live
+ *
+ * Live keys will survive a call to selection_cleanup().  @p need not be in the
+ * selection (if it is not then the call will be ignored).
+ */
 void selection_live(hash *h, const char *key) {
   int *ptr = hash_find(h, key);
 
@@ -61,6 +95,11 @@ static int selection_cleanup_callback(const char *key,
   return 0;
 }
 
+/** @brief Delete all non-live keys from a selection
+ * @param h Hash representing selection
+ *
+ * After cleanup, no keys are marked as live.
+ */
 void selection_cleanup(hash *h) {
   hash_foreach(h, selection_cleanup_callback, h);
 }
@@ -72,6 +111,9 @@ static int selection_empty_callback(const char *key,
   return 0;
 }
 
+/** @brief Remove all keys from a selection
+ * @param h Hash representing selection
+ */
 void selection_empty(hash *h) {
   hash_foreach(h, selection_empty_callback, h);
 }
index e625e8930050c5bb0972a00595073acf7c04b2b0..90896495f1e5901c2af988f24f2ce0418ac990a0 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2006 Richard Kettlewell
+ * Copyright (C) 2006, 2007 Richard Kettlewell
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -17,6 +17,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/selection.c
+ * @brief Select management for Disobedience
+ */
 
 #ifndef SELECTION_H
 #define SELECTION_H
index e09f037c91770efda46fc87498a84ad92bd54644..245ee26b6761261126a7acb38d12bf4a236839fe 100644 (file)
@@ -17,6 +17,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/sink.c
+ * @brief Abstract output sink type
+ */
 
 #include <config.h>
 #include "types.h"
 #include "log.h"
 #include "printf.h"
 
+/** @brief Formatted output to a sink
+ * @param s Sink to write to
+ * @param fmt Format string
+ * @param ap Argument list
+ * @return Number of bytes written on success, -1 on error
+ */
 int sink_vprintf(struct sink *s, const char *fmt, va_list ap) {
   return byte_vsinkprintf(s, fmt, ap);
 }
 
+/** @brief Formatted output to a sink
+ * @param s Sink to write to
+ * @param fmt Format string
+ * @return Number of bytes written on success, -1 on error
+ */
 int sink_printf(struct sink *s, const char *fmt, ...) {
   va_list ap;
   int n;
@@ -48,14 +62,22 @@ int sink_printf(struct sink *s, const char *fmt, ...) {
 
 /* stdio sink *****************************************************************/
 
+/** @brief Sink that writes to a stdio @c FILE */
 struct stdio_sink {
+  /** @brief Base member */
   struct sink s;
+
+  /** @brief Filename */
   const char *name;
+
+  /** @brief Stream to write to */
   FILE *fp;
 };
 
+/** @brief Reinterpret a @ref sink as a @ref stdio_sink */
 #define S(s) ((struct stdio_sink *)s)
 
+/** @brief Write callback for @ref stdio_sink */
 static int sink_stdio_write(struct sink *s, const void *buffer, int nbytes) {
   int n = fwrite(buffer, 1, nbytes, S(s)->fp);
   if(n < nbytes) {
@@ -67,6 +89,11 @@ static int sink_stdio_write(struct sink *s, const void *buffer, int nbytes) {
   return n;
 }
 
+/** @brief Create a sink that writes to a stdio stream
+ * @param name Filename for use in error messages
+ * @param fp Stream to write to
+ * @return Pointer to new sink
+ */
 struct sink *sink_stdio(const char *name, FILE *fp) {
   struct stdio_sink *s = xmalloc(sizeof *s);
 
@@ -78,16 +105,24 @@ struct sink *sink_stdio(const char *name, FILE *fp) {
 
 /* dynstr sink ****************************************************************/
 
+/** @brief Sink that writes to a dynamic string */
 struct dynstr_sink {
+  /** @brief Base member */
   struct sink s;
+  /** @brief Pointer to dynamic string to append to */
   struct dynstr *d;
 };
 
+/** @brief Write callback for @ref dynstr_sink */
 static int sink_dynstr_write(struct sink *s, const void *buffer, int nbytes) {
   dynstr_append_bytes(((struct dynstr_sink *)s)->d, buffer, nbytes);
   return nbytes;
 }
 
+/** @brief Create a sink that appends to a @ref dynstr
+ * @param output Dynamic string to append to
+ * @return Pointer to new sink
+ */
 struct sink *sink_dynstr(struct dynstr *output) {
   struct dynstr_sink *s = xmalloc(sizeof *s);
 
index 291476e83bb47e4c07aeaaaaa3dce86607dbae72..61345010baaa598bdd59505fe61f364f66677b0e 100644 (file)
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/sink.h
+ * @brief Abstract output sink type
+ */
 
 #ifndef SINK_H
 #define SINK_H
 
-/* a sink is something you write to (the opposite would be a source) */
-
 struct dynstr;
 
+/** @brief Sink type
+ *
+ * A sink is something you write bytes to; the opposite would be a
+ * source.  We provide sink_stdio() and sink_dynstr() to create sinks
+ * to write to stdio streams and dynamic strings.
+ */
 struct sink {
+  /** @brief Write callback
+   * @param s Sink to write to
+   * @param buffer First byte to write
+   * @param nbytes Number of bytes to write
+   * @return non-negative on success, -1 on error
+   */
   int (*write)(struct sink *s, const void *buffer, int nbytes);
-  /* return >= 0 on success, -1 on error */
 };
 
 struct sink *sink_stdio(const char *name, FILE *fp);
@@ -43,14 +55,30 @@ int sink_printf(struct sink *s, const char *fmt, ...)
   attribute((format (printf, 2, 3)));
 /* equivalent of vfprintf/fprintf for sink @s@ */
 
+/** @brief Write bytes to a sink
+ * @param s Sink to write to
+ * @param buffer First byte to write
+ * @param nbytes Number of bytes to write
+ * @return non-negative on success, -1 on error
+ */
 static inline int sink_write(struct sink *s, const void *buffer, int nbytes) {
   return s->write(s, buffer, nbytes);
 }
 
+/** @brief Write string to a sink
+ * @param s Sink to write to
+ * @param str String to write
+ * @return non-negative on success, -1 on error
+ */
 static inline int sink_writes(struct sink *s, const char *str) {
   return s->write(s, str, strlen(str));
 }
 
+/** @brief Write one byte to a sink
+ * @param s Sink to write to
+ * @param c Byte to write (as a @c char)
+ * @return non-negative on success, -1 on error
+ */
 static inline int sink_writec(struct sink *s, char c) {
   return s->write(s, &c, 1);
 }
index 1bb19a5c9b5ad63fa710cae3ec6de2f8be4ab8da..e39506b9c132d4ef8cc7124f13e92bd00bbc0e59 100644 (file)
@@ -56,7 +56,8 @@ char **split(const char *p,
   size_t l;
   int qc;
 
-  if(!error_handler) error_handler = no_error_handler;
+  if(!error_handler)
+    error_handler = no_error_handler;
   vector_init(&v);
   while(*p && !(*p == '#' && (flags & SPLIT_COMMENTS))) {
     if(space(*p)) {
index 3ae97c87df108045830af0f4ed47dbac37a6bf75..399c7f14907a6673f38efb289eaac7e362bbdec7 100644 (file)
 #include <assert.h>
 #include <sys/types.h>
 #include <sys/stat.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include <stddef.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <netinet/in.h>
 
-#include "utf8.h"
 #include "mem.h"
 #include "log.h"
 #include "vector.h"
 #include "unicode.h"
 #include "inputline.h"
 #include "wstat.h"
+#include "signame.h"
+#include "cache.h"
+#include "filepart.h"
+#include "hash.h"
+#include "selection.h"
+#include "syscalls.h"
+#include "kvp.h"
+#include "sink.h"
+#include "printf.h"
+#include "basen.h"
+#include "split.h"
+#include "configuration.h"
+#include "addr.h"
 
 static int tests, errors;
 static int fail_first;
@@ -94,22 +113,48 @@ static const char *format_utf32(const uint32_t *s) {
   return d.vec;
 }
 
-#define check_string(GOT, WANT) do {                           \
-  const char *g = GOT;                                         \
-  const char *w = WANT;                                                \
-                                                               \
-  if(w == 0) {                                                 \
-    fprintf(stderr, "%s:%d: %s returned 0\n",                  \
-            __FILE__, __LINE__, #GOT);                         \
-    count_error();                                                     \
-  } else if(strcmp(w, g)) {                                    \
-    fprintf(stderr, "%s:%d: %s returned:\n%s\nexpected:\n%s\n",        \
-           __FILE__, __LINE__, #GOT, format(g), format(w));    \
-    count_error();                                                     \
-  }                                                            \
-  ++tests;                                                     \
+#define check_string(GOT, WANT) do {                                    \
+  const char *got = GOT;                                                \
+  const char *want = WANT;                                              \
+                                                                        \
+  if(want == 0) {                                                       \
+    fprintf(stderr, "%s:%d: %s returned 0\n",                           \
+            __FILE__, __LINE__, #GOT);                                  \
+    count_error();                                                      \
+  } else if(strcmp(want, got)) {                                        \
+    fprintf(stderr, "%s:%d: %s returned:\n%s\nexpected:\n%s\n",         \
+           __FILE__, __LINE__, #GOT, format(got), format(want));       \
+    count_error();                                                      \
+  }                                                                     \
+  ++tests;                                                              \
  } while(0)
 
+#define check_string_prefix(GOT, WANT) do {                             \
+  const char *got = GOT;                                                \
+  const char *want = WANT;                                              \
+                                                                        \
+  if(want == 0) {                                                       \
+    fprintf(stderr, "%s:%d: %s returned 0\n",                           \
+            __FILE__, __LINE__, #GOT);                                  \
+    count_error();                                                      \
+  } else if(strncmp(want, got, strlen(want))) {                         \
+    fprintf(stderr, "%s:%d: %s returned:\n%s\nexpected:\n%s...\n",      \
+           __FILE__, __LINE__, #GOT, format(got), format(want));       \
+    count_error();                                                      \
+  }                                                                     \
+  ++tests;                                                              \
+ } while(0)
+
+#define check_integer(GOT, WANT) do {                           \
+  const intmax_t got = GOT, want = WANT;                        \
+  if(got != want) {                                             \
+    fprintf(stderr, "%s:%d: %s returned: %jd  expected: %jd\n", \
+            __FILE__, __LINE__, #GOT, got, want);               \
+    count_error();                                              \
+  }                                                             \
+  ++tests;                                                      \
+} while(0)
+
 static uint32_t *ucs4parse(const char *s) {
   struct dynstr_ucs4 d;
   char *e;
@@ -139,7 +184,7 @@ static void test_utf8(void) {
   insist(!utf32_cmp(w, ucs));                  \
   u8 = utf32_to_utf8(ucs, utf32_len(ucs), 0);   \
   insist(u8 != 0);                             \
-  insist(!strcmp(u8, CHARS));                  \
+  check_string(u8, CHARS);                     \
 } while(0)
 
   fprintf(stderr, "test_utf8\n");
@@ -214,6 +259,7 @@ static void test_utf8(void) {
   U8("\xF4\x80\x80\x80", "0x100000");
   U8("\xF4\x8F\xBF\xBF", "0x10FFFF");
   insist(!validutf8("\xF4\x90\x80\x80"));
+  insist(!validutf8("\xF4\x80\xFF\x80"));
 
   /* miscellaneous non-UTF-8 rubbish */
   insist(!validutf8("\x80"));
@@ -239,38 +285,163 @@ static void test_utf8(void) {
   insist(!validutf8("\xF8"));
 }
 
+static int test_multipart_callback(const char *s, void *u) {
+  struct vector *parts = u;
+
+  vector_append(parts, (char *)s);
+  return 0;
+}
+
 static void test_mime(void) {
   char *t, *n, *v;
+  struct vector parts[1];
 
   fprintf(stderr, "test_mime\n");
 
   t = n = v = 0;
   insist(!mime_content_type("text/plain", &t, &n, &v));
-  insist(!strcmp(t, "text/plain"));
+  check_string(t, "text/plain");
   insist(n == 0);
   insist(v == 0);
 
+  insist(mime_content_type("TEXT ((broken) comment", &t, &n, &v) < 0);
+  insist(mime_content_type("TEXT ((broken) comment\\", &t, &n, &v) < 0);
+  
   t = n = v = 0;
-  insist(!mime_content_type("TEXT ((nested) comment) /plain", &t, &n, &v));
-  insist(!strcmp(t, "text/plain"));
+  insist(!mime_content_type("TEXT ((nested)\\ comment) /plain", &t, &n, &v));
+  check_string(t, "text/plain");
   insist(n == 0);
   insist(v == 0);
 
   t = n = v = 0;
-  insist(!mime_content_type(" text/plain ; Charset=utf-8", &t, &n, &v));
-  insist(!strcmp(t, "text/plain"));
-  insist(!strcmp(n, "charset"));
-  insist(!strcmp(v, "utf-8"));
+  insist(!mime_content_type(" text/plain ; Charset=\"utf-\\8\"", &t, &n, &v));
+  check_string(t, "text/plain");
+  check_string(n, "charset");
+  check_string(v, "utf-8");
 
   t = n = v = 0;
   insist(!mime_content_type("text/plain;charset = ISO-8859-1 ", &t, &n, &v));
-  insist(!strcmp(t, "text/plain"));
-  insist(!strcmp(n, "charset"));
-  insist(!strcmp(v, "ISO-8859-1"));
+  check_string(t, "text/plain");
+  check_string(n, "charset");
+  check_string(v, "ISO-8859-1");
+
+  t = n = v = 0;
+  insist(!mime_rfc2388_content_disposition("form-data; name=\"field1\"", &t, &n, &v));
+  check_string(t, "form-data");
+  check_string(n, "name");
+  check_string(v, "field1");
 
+  insist(!mime_rfc2388_content_disposition("inline", &t, &n, &v));
+  check_string(t, "inline");
+  insist(n == 0);
+  insist(v == 0);
+
+  /* Current versions of the code only understand a single arg to these
+   * headers.  This is a bug at the level they work at but suffices for
+   * DisOrder's current purposes. */
+
+  insist(!mime_rfc2388_content_disposition(
+              "attachment; filename=genome.jpeg;\n"
+              "modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"",
+         &t, &n, &v));
+  check_string(t, "attachment");
+  check_string(n, "filename");
+  check_string(v, "genome.jpeg");
+
+  vector_init(parts);
+  insist(mime_multipart("--outer\r\n"
+                        "Content-Type: text/plain\r\n"
+                        "Content-Disposition: inline\r\n"
+                        "Content-Description: text-part-1\r\n"
+                        "\r\n"
+                        "Some text goes here\r\n"
+                        "\r\n"
+                        "--outer\r\n"
+                        "Content-Type: multipart/mixed; boundary=inner\r\n"
+                        "Content-Disposition: attachment\r\n"
+                        "Content-Description: multipart-2\r\n"
+                        "\r\n"
+                        "--inner\r\n"
+                        "Content-Type: text/plain\r\n"
+                        "Content-Disposition: inline\r\n"
+                        "Content-Description: text-part-2\r\n"
+                        "\r\n"
+                        "Some more text here.\r\n"
+                        "\r\n"
+                        "--inner\r\n"
+                        "Content-Type: image/jpeg\r\n"
+                        "Content-Disposition: attachment\r\n"
+                        "Content-Description: jpeg-1\r\n"
+                        "\r\n"
+                        "<jpeg data>\r\n"
+                        "--inner--\r\n"
+                        "--outer--\r\n",
+                        test_multipart_callback,
+                        "outer",
+                        parts) == 0);
+  check_integer(parts->nvec, 2);
+  check_string(parts->vec[0],
+               "Content-Type: text/plain\r\n"
+               "Content-Disposition: inline\r\n"
+               "Content-Description: text-part-1\r\n"
+               "\r\n"
+               "Some text goes here\r\n");
+  check_string(parts->vec[1],
+               "Content-Type: multipart/mixed; boundary=inner\r\n"
+               "Content-Disposition: attachment\r\n"
+               "Content-Description: multipart-2\r\n"
+               "\r\n"
+               "--inner\r\n"
+               "Content-Type: text/plain\r\n"
+               "Content-Disposition: inline\r\n"
+               "Content-Description: text-part-2\r\n"
+               "\r\n"
+               "Some more text here.\r\n"
+               "\r\n"
+               "--inner\r\n"
+               "Content-Type: image/jpeg\r\n"
+               "Content-Disposition: attachment\r\n"
+               "Content-Description: jpeg-1\r\n"
+               "\r\n"
+               "<jpeg data>\r\n"
+               "--inner--");
+  /* No trailing CRLF is _correct_ - see RFC2046 5.1.1 note regarding CRLF
+   * preceding the boundary delimiter line.  An implication of this is that we
+   * must cope with partial lines at the end of the input when recursively
+   * decomposing a multipart message. */
+  vector_init(parts);
+  insist(mime_multipart("--inner\r\n"
+                        "Content-Type: text/plain\r\n"
+                        "Content-Disposition: inline\r\n"
+                        "Content-Description: text-part-2\r\n"
+                        "\r\n"
+                        "Some more text here.\r\n"
+                        "\r\n"
+                        "--inner\r\n"
+                        "Content-Type: image/jpeg\r\n"
+                        "Content-Disposition: attachment\r\n"
+                        "Content-Description: jpeg-1\r\n"
+                        "\r\n"
+                        "<jpeg data>\r\n"
+                        "--inner--",
+                        test_multipart_callback,
+                        "inner",
+                        parts) == 0);
+  check_integer(parts->nvec, 2);
+  check_string(parts->vec[0],
+               "Content-Type: text/plain\r\n"
+               "Content-Disposition: inline\r\n"
+               "Content-Description: text-part-2\r\n"
+               "\r\n"
+               "Some more text here.\r\n");
+  check_string(parts->vec[1],
+               "Content-Type: image/jpeg\r\n"
+               "Content-Disposition: attachment\r\n"
+               "Content-Description: jpeg-1\r\n"
+               "\r\n"
+               "<jpeg data>");
   /* XXX mime_parse */
-  /* XXX mime_multipart */
-  /* XXX mime_rfc2388_content_disposition */
 
   check_string(mime_qp(""), "");
   check_string(mime_qp("foobar"), "foobar");
@@ -428,8 +599,8 @@ static void test_casefold(void) {
       l = 0x3BC;                       /* GREEK SMALL LETTER MU */
       break;
     case 0xDF:                         /* LATIN SMALL LETTER SHARP S */
-      insist(!strcmp(canon_folded, "ss"));
-      insist(!strcmp(compat_folded, "ss"));
+      check_string(canon_folded, "ss");
+      check_string(compat_folded, "ss");
       l = 0;
       break;
     }
@@ -717,9 +888,498 @@ static void test_unicode(void) {
   fclose(fp);
   breaktest("auxiliary/GraphemeBreakTest.txt", utf32_is_grapheme_boundary);
   breaktest("auxiliary/WordBreakTest.txt", utf32_is_word_boundary);
+  insist(utf32_combining_class(0x40000) == 0);
+  insist(utf32_combining_class(0xE0000) == 0);
+}
+
+static void test_signame(void) {
+  fprintf(stderr, "test_signame\n");
+  insist(find_signal("SIGTERM") == SIGTERM);
+  insist(find_signal("SIGHUP") == SIGHUP);
+  insist(find_signal("SIGINT") == SIGINT);
+  insist(find_signal("SIGQUIT") == SIGQUIT);
+  insist(find_signal("SIGKILL") == SIGKILL);
+  insist(find_signal("SIGYOURMUM") == -1);
+}
+
+static void test_cache(void) {
+  const struct cache_type t1 = { 1 }, t2 = { 10 };
+  const char v11[] = "spong", v12[] = "wibble", v2[] = "blat";
+  fprintf(stderr, "test_cache\n");
+  cache_put(&t1, "1_1", v11);
+  cache_put(&t1, "1_2", v12);
+  cache_put(&t2, "2", v2);
+  insist(cache_count() == 3);
+  insist(cache_get(&t2, "2") == v2);
+  insist(cache_get(&t1, "1_1") == v11);
+  insist(cache_get(&t1, "1_2") == v12);
+  insist(cache_get(&t1, "2") == 0);
+  insist(cache_get(&t2, "1_1") == 0);
+  insist(cache_get(&t2, "1_2") == 0);
+  insist(cache_get(&t1, "2") == 0);
+  insist(cache_get(&t2, "1_1") == 0);
+  insist(cache_get(&t2, "1_2") == 0);
+  sleep(2);
+  cache_expire();
+  insist(cache_count() == 1);
+  insist(cache_get(&t1, "1_1") == 0);
+  insist(cache_get(&t1, "1_2") == 0);
+  insist(cache_get(&t2, "2") == v2);
+  cache_clean(0);
+  insist(cache_count() == 0);
+  insist(cache_get(&t2, "2") == 0); 
+}
+
+static void test_filepart(void) {
+  fprintf(stderr, "test_filepart\n");
+  check_string(d_dirname("/"), "/");
+  check_string(d_dirname("////"), "/");
+  check_string(d_dirname("/spong"), "/");
+  check_string(d_dirname("////spong"), "/");
+  check_string(d_dirname("/foo/bar"), "/foo");
+  check_string(d_dirname("////foo/////bar"), "////foo");
+  check_string(d_dirname("./bar"), ".");
+  check_string(d_dirname(".//bar"), ".");
+  check_string(d_dirname("."), ".");
+  check_string(d_dirname(".."), ".");
+  check_string(d_dirname("../blat"), "..");
+  check_string(d_dirname("..//blat"), "..");
+  check_string(d_dirname("wibble"), ".");
+  check_string(extension("foo.c"), ".c");
+  check_string(extension(".c"), ".c");
+  check_string(extension("."), ".");
+  check_string(extension("foo"), "");
+  check_string(extension("./foo"), "");
+  check_string(extension("./foo.c"), ".c");
+  check_string(strip_extension("foo.c"), "foo");
+  check_string(strip_extension("foo.mp3"), "foo");
+  check_string(strip_extension("foo.---"), "foo.---");
+  check_string(strip_extension("foo.---xyz"), "foo.---xyz");
+  check_string(strip_extension("foo.bar/wibble.spong"), "foo.bar/wibble");
+}
+
+static void test_selection(void) {
+  hash *h;
+  fprintf(stderr, "test_selection\n");
+  insist((h = selection_new()) != 0);
+  selection_set(h, "one", 1);
+  selection_set(h, "two", 1);
+  selection_set(h, "three", 0);
+  selection_set(h, "four", 1);
+  insist(selection_selected(h, "one") == 1);
+  insist(selection_selected(h, "two") == 1);
+  insist(selection_selected(h, "three") == 0);
+  insist(selection_selected(h, "four") == 1);
+  insist(selection_selected(h, "five") == 0);
+  insist(hash_count(h) == 3);
+  selection_flip(h, "one"); 
+  selection_flip(h, "three"); 
+  insist(selection_selected(h, "one") == 0);
+  insist(selection_selected(h, "three") == 1);
+  insist(hash_count(h) == 3);
+  selection_live(h, "one");
+  selection_live(h, "two");
+  selection_live(h, "three");
+  selection_cleanup(h);
+  insist(selection_selected(h, "one") == 0);
+  insist(selection_selected(h, "two") == 1);
+  insist(selection_selected(h, "three") == 1);
+  insist(selection_selected(h, "four") == 0);
+  insist(selection_selected(h, "five") == 0);
+  insist(hash_count(h) == 2);
+  selection_empty(h);
+  insist(selection_selected(h, "one") == 0);
+  insist(selection_selected(h, "two") == 0);
+  insist(selection_selected(h, "three") == 0);
+  insist(selection_selected(h, "four") == 0);
+  insist(selection_selected(h, "five") == 0);
+  insist(hash_count(h) == 0);
+}
+
+static void test_wstat(void) {
+  pid_t pid;
+  int w;
+  
+  fprintf(stderr, "test_wstat\n");
+  if(!(pid = xfork())) {
+    _exit(1);
+  }
+  while(waitpid(pid, &w, 0) < 0 && errno == EINTR)
+    ;
+  check_string(wstat(w), "exited with status 1");
+  if(!(pid = xfork())) {
+    kill(getpid(), SIGTERM);
+    _exit(-1);
+  }
+  while(waitpid(pid, &w, 0) < 0 && errno == EINTR)
+    ;
+  check_string_prefix(wstat(w), "terminated by signal 15");
+}
+
+static void test_kvp(void) {
+  struct kvp *k;
+  size_t n;
+  
+  fprintf(stderr, "test_kvp\n");
+  /* decoding */
+#define KVP_URLDECODE(S) kvp_urldecode((S), strlen(S))
+  insist(KVP_URLDECODE("=%zz") == 0);
+  insist(KVP_URLDECODE("=%0") == 0);
+  insist(KVP_URLDECODE("=%0z") == 0);
+  insist(KVP_URLDECODE("=%%") == 0);
+  insist(KVP_URLDECODE("==%") == 0);
+  insist(KVP_URLDECODE("wibble") == 0);
+  insist(KVP_URLDECODE("") == 0);
+  insist(KVP_URLDECODE("wibble&") == 0);
+  insist((k = KVP_URLDECODE("one=bl%61t+foo")) != 0);
+  check_string(kvp_get(k, "one"), "blat foo");
+  insist(kvp_get(k, "ONE") == 0);
+  insist(k->next == 0);
+  insist((k = KVP_URLDECODE("wibble=splat&bar=spong")) != 0);
+  check_string(kvp_get(k, "wibble"), "splat");
+  check_string(kvp_get(k, "bar"), "spong");
+  insist(kvp_get(k, "ONE") == 0);
+  insist(k->next->next == 0);
+  /* encoding */
+  insist(kvp_set(&k, "bar", "spong") == 0);
+  insist(kvp_set(&k, "bar", "foo") == 1);
+  insist(kvp_set(&k, "zog", "%") == 1);
+  insist(kvp_set(&k, "wibble", 0) == 1);
+  insist(kvp_set(&k, "wibble", 0) == 0);
+  check_string(kvp_urlencode(k, 0),
+               "bar=foo&zog=%25");
+  check_string(kvp_urlencode(k, &n),
+               "bar=foo&zog=%25");
+  insist(n == strlen("bar=foo&zog=%25"));
+  check_string(urlencodestring("abc% +\n"),
+               "abc%25%20%2b%0a");
+}
+
+static void test_sink(void) {
+  struct sink *s;
+  struct dynstr d[1];
+  FILE *fp;
+  char *l;
+  
+  fprintf(stderr, "test_sink\n");
+
+  fp = tmpfile();
+  assert(fp != 0);
+  s = sink_stdio("tmpfile", fp);
+  insist(sink_printf(s, "test: %d\n", 999) == 10);
+  insist(sink_printf(s, "wibble: %s\n", "foobar") == 15);
+  rewind(fp);
+  insist(inputline("tmpfile", fp, &l, '\n') == 0);
+  check_string(l, "test: 999");
+  insist(inputline("tmpfile", fp, &l, '\n') == 0);
+  check_string(l, "wibble: foobar");
+  insist(inputline("tmpfile", fp, &l, '\n') == -1);
+  
+  dynstr_init(d);
+  s = sink_dynstr(d);
+  insist(sink_printf(s, "test: %d\n", 999) == 10);
+  insist(sink_printf(s, "wibble: %s\n", "foobar") == 15);
+  dynstr_terminate(d);
+  check_string(d->vec, "test: 999\nwibble: foobar\n");
+}
+
+static const char *do_printf(const char *fmt, ...) {
+  va_list ap;
+  char *s;
+  int rc;
+
+  va_start(ap, fmt);
+  rc = byte_vasprintf(&s, fmt, ap);
+  va_end(ap);
+  if(rc < 0)
+    return 0;
+  return s;
+}
+
+static void test_printf(void) {
+  char c;
+  short s;
+  int i;
+  long l;
+  long long ll;
+  intmax_t m;
+  ssize_t ssz;
+  ptrdiff_t p;
+  char *cp;
+  char buffer[16];
+  
+  fprintf(stderr, "test_printf\n");
+  check_string(do_printf("%d", 999), "999");
+  check_string(do_printf("%d", -999), "-999");
+  check_string(do_printf("%i", 999), "999");
+  check_string(do_printf("%i", -999), "-999");
+  check_string(do_printf("%u", 999), "999");
+  check_string(do_printf("%2u", 999), "999");
+  check_string(do_printf("%10u", 999), "       999");
+  check_string(do_printf("%-10u", 999), "999       ");
+  check_string(do_printf("%010u", 999), "0000000999");
+  check_string(do_printf("%-10d", -999), "-999      ");
+  check_string(do_printf("%-010d", -999), "-999      "); /* "-" beats "0" */
+  check_string(do_printf("%66u", 999), "                                                               999");
+  check_string(do_printf("%o", 999), "1747");
+  check_string(do_printf("%#o", 999), "01747");
+  check_string(do_printf("%#o", 0), "0");
+  check_string(do_printf("%x", 999), "3e7");
+  check_string(do_printf("%#x", 999), "0x3e7");
+  check_string(do_printf("%#X", 999), "0X3E7");
+  check_string(do_printf("%#x", 0), "0");
+  check_string(do_printf("%hd", (short)999), "999");
+  check_string(do_printf("%hhd", (short)99), "99");
+  check_string(do_printf("%ld", 100000L), "100000");
+  check_string(do_printf("%lld", 10000000000LL), "10000000000");
+  check_string(do_printf("%qd", 10000000000LL), "10000000000");
+  check_string(do_printf("%jd", (intmax_t)10000000000LL), "10000000000");
+  check_string(do_printf("%zd", (ssize_t)2000000000), "2000000000");
+  check_string(do_printf("%td", (ptrdiff_t)2000000000), "2000000000");
+  check_string(do_printf("%hu", (short)999), "999");
+  check_string(do_printf("%hhu", (short)99), "99");
+  check_string(do_printf("%lu", 100000L), "100000");
+  check_string(do_printf("%llu", 10000000000LL), "10000000000");
+  check_string(do_printf("%ju", (uintmax_t)10000000000LL), "10000000000");
+  check_string(do_printf("%zu", (size_t)2000000000), "2000000000");
+  check_string(do_printf("%tu", (ptrdiff_t)2000000000), "2000000000");
+  check_string(do_printf("%p", (void *)0x100), "0x100");
+  check_string(do_printf("%s", "wibble"), "wibble");
+  check_string(do_printf("%s-%s", "wibble", "wobble"), "wibble-wobble");
+  check_string(do_printf("%10s", "wibble"), "    wibble");
+  check_string(do_printf("%010s", "wibble"), "    wibble"); /* 0 ignored for %s */
+  check_string(do_printf("%-10s", "wibble"), "wibble    ");
+  check_string(do_printf("%2s", "wibble"), "wibble");
+  check_string(do_printf("%.2s", "wibble"), "wi");
+  check_string(do_printf("%.2s", "w"), "w");
+  check_string(do_printf("%4.2s", "wibble"), "  wi");
+  check_string(do_printf("%c", 'a'), "a");
+  check_string(do_printf("%4c", 'a'), "   a");
+  check_string(do_printf("%-4c", 'a'), "a   ");
+  check_string(do_printf("%*c", 0, 'a'), "a");
+  check_string(do_printf("x%hhny", &c), "xy");
+  insist(c == 1);
+  check_string(do_printf("xx%hnyy", &s), "xxyy");
+  insist(s == 2);
+  check_string(do_printf("xxx%nyyy", &i), "xxxyyy");
+  insist(i == 3);
+  check_string(do_printf("xxxx%lnyyyy", &l), "xxxxyyyy");
+  insist(l == 4);
+  check_string(do_printf("xxxxx%llnyyyyy", &ll), "xxxxxyyyyy");
+  insist(ll == 5);
+  check_string(do_printf("xxxxxx%jnyyyyyy", &m), "xxxxxxyyyyyy");
+  insist(m == 6);
+  check_string(do_printf("xxxxxxx%znyyyyyyy", &ssz), "xxxxxxxyyyyyyy");
+  insist(ssz == 7);
+  check_string(do_printf("xxxxxxxx%tnyyyyyyyy", &p), "xxxxxxxxyyyyyyyy");
+  insist(p == 8);
+  check_string(do_printf("%*d", 5, 99), "   99");
+  check_string(do_printf("%*d", -5, 99), "99   ");
+  check_string(do_printf("%.*d", 5, 99), "00099");
+  check_string(do_printf("%.*d", -5, 99), "99");
+  check_string(do_printf("%.0d", 0), "");
+  check_string(do_printf("%.d", 0), "");
+  check_string(do_printf("%.d", 0), "");
+  check_string(do_printf("%%"), "%");
+  check_string(do_printf("wibble"), "wibble");
+  insist(do_printf("%") == 0);
+  insist(do_printf("%=") == 0);
+  i = byte_asprintf(&cp, "xyzzy %d", 999);
+  insist(i == 9);
+  check_string(cp, "xyzzy 999");
+  i = byte_snprintf(buffer, sizeof buffer, "xyzzy %d", 999);
+  insist(i == 9);
+  check_string(buffer, "xyzzy 999");
+  i = byte_snprintf(buffer, sizeof buffer, "%*d", 32, 99);
+  insist(i == 32);
+  check_string(buffer, "               ");
+  {
+    /* bizarre workaround for compiler checking of format strings */
+    char f[] = "xyzzy %";
+    i = byte_asprintf(&cp, f);
+    insist(i == -1);
+  }
+}
+
+static void test_basen(void) {
+  unsigned long v[64];
+  char buffer[1024];
+
+  fprintf(stderr, "test_basen\n");
+  v[0] = 999;
+  insist(basen(v, 1, buffer, sizeof buffer, 10) == 0);
+  check_string(buffer, "999");
+
+  v[0] = 1+2*7+3*7*7+4*7*7*7;
+  insist(basen(v, 1, buffer, sizeof buffer, 7) == 0);
+  check_string(buffer, "4321");
+
+  v[0] = 0x00010203;
+  v[1] = 0x04050607;
+  v[2] = 0x08090A0B;
+  v[3] = 0x0C0D0E0F;
+  insist(basen(v, 4, buffer, sizeof buffer, 256) == 0);
+  check_string(buffer, "123456789abcdef");
+
+  v[0] = 0x00010203;
+  v[1] = 0x04050607;
+  v[2] = 0x08090A0B;
+  v[3] = 0x0C0D0E0F;
+  insist(basen(v, 4, buffer, sizeof buffer, 16) == 0);
+  check_string(buffer, "102030405060708090a0b0c0d0e0f");
+
+  v[0] = 0x00010203;
+  v[1] = 0x04050607;
+  v[2] = 0x08090A0B;
+  v[3] = 0x0C0D0E0F;
+  insist(basen(v, 4, buffer, 10, 16) == -1);
+}
+
+static void test_split(void) {
+  char **v;
+  int nv;
+
+  fprintf(stderr, "test_split\n");
+  insist(split("\"misquoted", &nv, SPLIT_COMMENTS|SPLIT_QUOTES, 0, 0) == 0);
+  insist(split("\'misquoted", &nv, SPLIT_COMMENTS|SPLIT_QUOTES, 0, 0) == 0);
+  insist(split("\'misquoted\\", &nv, SPLIT_COMMENTS|SPLIT_QUOTES, 0, 0) == 0);
+  insist(split("\'misquoted\\\"", &nv, SPLIT_COMMENTS|SPLIT_QUOTES, 0, 0) == 0);
+  insist(split("\'mis\\escaped\'", &nv, SPLIT_COMMENTS|SPLIT_QUOTES, 0, 0) == 0);
+
+  insist((v = split("", &nv, SPLIT_COMMENTS|SPLIT_QUOTES, 0, 0)));
+  check_integer(nv, 0);
+  insist(*v == 0);
+
+  insist((v = split("wibble", &nv, SPLIT_COMMENTS|SPLIT_QUOTES, 0, 0)));
+  check_integer(nv, 1);
+  check_string(v[0], "wibble");
+  insist(v[1] == 0);
+
+  insist((v = split("   wibble \t\r\n wobble   ", &nv,
+                    SPLIT_COMMENTS|SPLIT_QUOTES, 0, 0)));
+  check_integer(nv, 2);
+  check_string(v[0], "wibble");
+  check_string(v[1], "wobble");
+  insist(v[2] == 0);
+
+  insist((v = split("wibble wobble #splat", &nv,
+                    SPLIT_COMMENTS|SPLIT_QUOTES, 0, 0)));
+  check_integer(nv, 2);
+  check_string(v[0], "wibble");
+  check_string(v[1], "wobble");
+  insist(v[2] == 0);
+
+  insist((v = split("\"wibble wobble\" #splat", &nv,
+                    SPLIT_COMMENTS|SPLIT_QUOTES, 0, 0)));
+  check_integer(nv, 1);
+  check_string(v[0], "wibble wobble");
+  insist(v[1] == 0);
+
+  insist((v = split("\"wibble \\\"\\nwobble\"", &nv,
+                    SPLIT_COMMENTS|SPLIT_QUOTES, 0, 0)));
+  check_integer(nv, 1);
+  check_string(v[0], "wibble \"\nwobble");
+  insist(v[1] == 0);
+
+  insist((v = split("\"wibble wobble\" #splat", &nv,
+                    SPLIT_QUOTES, 0, 0)));
+  check_integer(nv, 2);
+  check_string(v[0], "wibble wobble");
+  check_string(v[1], "#splat");
+  insist(v[2] == 0);
+
+  insist((v = split("\"wibble wobble\" #splat", &nv,
+                    SPLIT_COMMENTS, 0, 0)));
+  check_integer(nv, 2);
+  check_string(v[0], "\"wibble");
+  check_string(v[1], "wobble\"");
+  insist(v[2] == 0);
+
+  check_string(quoteutf8("wibble"), "wibble");
+  check_string(quoteutf8("  wibble  "), "\"  wibble  \"");
+  check_string(quoteutf8("wibble wobble"), "\"wibble wobble\"");
+  check_string(quoteutf8("wibble\"wobble"), "\"wibble\\\"wobble\"");
+  check_string(quoteutf8("wibble\nwobble"), "\"wibble\\nwobble\"");
+  check_string(quoteutf8("wibble\\wobble"), "\"wibble\\\\wobble\"");
+  check_string(quoteutf8("wibble'wobble"), "\"wibble'wobble\"");
+}
+
+static void test_hash(void) {
+  hash *h;
+  int i, *ip;
+  char **keys;
+
+  fprintf(stderr, "test_hash\n");
+  h = hash_new(sizeof(int));
+  for(i = 0; i < 10000; ++i)
+    insist(hash_add(h, do_printf("%d", i), &i, HASH_INSERT) == 0);
+  check_integer(hash_count(h), 10000);
+  for(i = 0; i < 10000; ++i) {
+    insist((ip = hash_find(h, do_printf("%d", i))) != 0);
+    check_integer(*ip, i);
+    insist(hash_add(h, do_printf("%d", i), &i, HASH_REPLACE) == 0);
+  }
+  check_integer(hash_count(h), 10000);
+  keys = hash_keys(h);
+  for(i = 0; i < 10000; ++i)
+    insist(keys[i] != 0);
+  insist(keys[10000] == 0);
+  for(i = 0; i < 10000; ++i)
+    insist(hash_remove(h, do_printf("%d", i)) == 0);
+  check_integer(hash_count(h), 0);
+}
+
+static void test_addr(void) {
+  struct stringlist a;
+  const char *s[2];
+  struct addrinfo *ai;
+  char *name;
+  const struct sockaddr_in *sin;
+
+  static const struct addrinfo pref = {
+    AI_PASSIVE,
+    PF_INET,
+    SOCK_STREAM,
+    0,
+    0,
+    0,
+    0,
+    0
+  };
+
+  a.n = 1;
+  a.s = (char **)s;
+  s[0] = "smtp";
+  ai = get_address(&a, &pref, &name);
+  insist(ai != 0);
+  check_integer(ai->ai_family, PF_INET);
+  check_integer(ai->ai_socktype, SOCK_STREAM);
+  check_integer(ai->ai_protocol, IPPROTO_TCP);
+  check_integer(ai->ai_addrlen, sizeof(struct sockaddr_in));
+  sin = (const struct sockaddr_in *)ai->ai_addr;
+  check_integer(sin->sin_family, AF_INET);
+  check_integer(sin->sin_addr.s_addr, 0);
+  check_integer(ntohs(sin->sin_port), 25);
+  check_string(name, "host * service smtp");
+
+  a.n = 2;
+  s[0] = "localhost";
+  s[1] = "nntp";
+  ai = get_address(&a, &pref, &name);
+  insist(ai != 0);
+  check_integer(ai->ai_family, PF_INET);
+  check_integer(ai->ai_socktype, SOCK_STREAM);
+  check_integer(ai->ai_protocol, IPPROTO_TCP);
+  check_integer(ai->ai_addrlen, sizeof(struct sockaddr_in));
+  sin = (const struct sockaddr_in *)ai->ai_addr;
+  check_integer(sin->sin_family, AF_INET);
+  check_integer(ntohl(sin->sin_addr.s_addr), 0x7F000001);
+  check_integer(ntohs(sin->sin_port), 119);
+  check_string(name, "host localhost service nntp");
 }
 
 int main(void) {
+  mem_init();
   fail_first = !!getenv("FAIL_FIRST");
   insist('\n' == 0x0A);
   insist('\r' == 0x0D);
@@ -731,13 +1391,17 @@ int main(void) {
   insist('a' == 0x61);
   insist('z' == 0x7A);
   /* addr.c */
+  test_addr();
   /* asprintf.c */
   /* authhash.c */
   /* basen.c */
+  test_basen();
   /* charset.c */
   /* client.c */
   /* configuration.c */
   /* event.c */
+  /* filepart.c */
+  test_filepart();
   /* fprintf.c */
   /* heap.c */
   test_heap();
@@ -745,6 +1409,7 @@ int main(void) {
   test_hex();
   /* inputline.c */
   /* kvp.c */
+  test_kvp();
   /* log.c */
   /* mem.c */
   /* mime.c */
@@ -753,10 +1418,13 @@ int main(void) {
   /* mixer.c */
   /* plugin.c */
   /* printf.c */
+  test_printf();
   /* queue.c */
   /* sink.c */
+  test_sink();
   /* snprintf.c */
   /* split.c */
+  test_split();
   /* syscalls.c */
   /* table.c */
   /* unicode.c */
@@ -767,8 +1435,15 @@ int main(void) {
   /* words.c */
   test_casefold();
   test_words();
-  /* XXX words() */
   /* wstat.c */
+  test_wstat();
+  /* signame.c */
+  test_signame();
+  /* cache.c */
+  test_cache();
+  /* selection.c */
+  test_selection();
+  test_hash();
   fprintf(stderr,  "%d errors out of %d tests\n", errors, tests);
   return !!errors;
 }
diff --git a/lib/utf8.c b/lib/utf8.c
deleted file mode 100644 (file)
index 9ca228c..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * This file is part of DisOrder
- * Copyright (C) 2005 Richard Kettlewell
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
- * USA
- */
-
-#include <config.h>
-#include "types.h"
-
-#include "utf8.h"
-
-/*
-Local Variables:
-c-basic-offset:2
-comment-column:40
-fill-column:79
-End:
-*/
diff --git a/lib/utf8.h b/lib/utf8.h
deleted file mode 100644 (file)
index 683a8a9..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * This file is part of DisOrder
- * Copyright (C) 2004, 2005 Richard Kettlewell
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
- * USA
- */
-#ifndef UTF8_H
-#define UTF8_H
-
-#define PARSE_UTF8(S,C,E) do {                 \
-  if((unsigned char)*S < 0x80)                 \
-    C = *S++;                                  \
-  else if((unsigned char)*S <= 0xDF) {         \
-    C = (*S++ & 0x1F) << 6;                    \
-    if((*S & 0xC0) != 0x80) { E; }             \
-    C |= (*S++ & 0x3F);                                \
-    if(C < 0x80) { E; }                                \
-  } else if((unsigned char)*S <= 0xEF) {       \
-    C = (*S++ & 0x0F) << 12;                   \
-    if((*S & 0xC0) != 0x80) { E; }             \
-    C |= (*S++ & 0x3F) << 6;                   \
-    if((*S & 0xC0) != 0x80) { E; }             \
-    C |= (*S++ & 0x3F);                                \
-    if(C < 0x800                               \
-       || (C >= 0xD800 && C <= 0xDFFF)) {      \
-      E;                                       \
-    }                                          \
-  } else if((unsigned char)*S <= 0xF7) {       \
-    C = (*S++ & 0x07) << 18;                   \
-    if((*S & 0xC0) != 0x80) { E; }             \
-    C |= (*S++ & 0x3F) << 12;                  \
-    if((*S & 0xC0) != 0x80) { E; }             \
-    C |= (*S++ & 0x3F) << 6;                   \
-    if((*S & 0xC0) != 0x80) { E; }             \
-    C |= (*S++ & 0x3F);                                \
-    if(C < 0x10000 || C > 0x10FFFF) { E; }     \
-  } else {                                     \
-    E;                                         \
-  }                                            \
-} while(0)
-
-#endif /* UTF8_h */
-
-/*
-Local Variables:
-c-basic-offset:2
-comment-column:40
-End:
-*/
index 6d4cd29b1470f6a8f4dab9dc5e474baa9eb64197..0fd124c66ed2988a1d453cd80c619c3bb07a7a99 100644 (file)
@@ -17,6 +17,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/wstat.c
+ * @brief Convert wait status to text
+ */
 
 #include <config.h>
 
 #include "wstat.h"
 #include "printf.h"
 
+/** @brief Convert exit status to text
+ * @param w Exit status (e.g. from waitpid())
+ * @return Allocated string containing description of status
+ */
 const char *wstat(int w) {
   int n;
   char *r;
index 5a145ecb9f9e270ea7ce2d6b920fcc499b479769..873b4f412bcee68b60193290d097bb6ec2325b57 100644 (file)
@@ -17,6 +17,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
+/** @file lib/wstat.h
+ * @brief Convert wait status to text
+ */
 
 #ifndef WSTAT_H
 #define WSTAT_H
index 95465c022777a0913895090311606fc1940566ab..0f16c1a54c82533b1ffe0c14a2b4857f881e606d 100644 (file)
@@ -1,5 +1,5 @@
 #
-# Copyright (C) 2004, 2005 Richard Kettlewell
+# Copyright (C) 2004, 2005, 2007 Richard Kettlewell
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -39,6 +39,11 @@ Example 2:
   for path in sys.argv[1:]:
     d.play(path)
 
+See disorder_protocol(5) for details of the communication protocol.
+
+NB that this code only supports servers configured to use SHA1-based
+authentication.  If the server demands another hash then it will not be
+possible to use this module.
 """
 
 import re
@@ -231,7 +236,7 @@ def _list2dict(l):
     while True:
       k = i.next()
       v = i.next()
-      d[k] = v
+      d[str(k)] = v
   except StopIteration:
     pass
   return d
@@ -294,7 +299,8 @@ class client:
       home = pw.pw_dir
     privconf = _configfile + "." + pw.pw_name
     passfile = home + os.sep + ".disorder" + os.sep + "passwd"
-    self._readfile(_configfile)
+    if os.path.exists(_configfile):
+      self._readfile(_configfile)
     if os.path.exists(privconf):
       self._readfile(privconf)
     if os.path.exists(passfile) and _userconf:
@@ -402,8 +408,14 @@ class client:
 
     Arguments:
     track -- the path of the track to play.
+
+    Returns the ID of the new queue entry.
+
+    Note that queue IDs are unicode strings (because all track information
+    values are unicode strings).
     """
-    self._simple("play", track)
+    res, details = self._simple("play", track)
+    return unicode(details)             # because it's unicode in queue() output
 
   def remove(self, track):
     """Remove a track from the queue.
@@ -471,7 +483,10 @@ class client:
   def playing(self):
     """Return the currently playing track.
 
-    If a track is playing then it is returned as a dictionary.
+    If a track is playing then it is returned as a dictionary.  See
+    disorder_protocol(5) for the meanings of the keys.  All keys are
+    plain strings but the values will be unicode strings.
+    
     If no track is playing then None is returned."""
     res, details = self._simple("playing")
     if res % 10 != 9:
@@ -493,14 +508,20 @@ class client:
     """Return a list of recently played tracks.
 
     The return value is a list of dictionaries corresponding to
-    recently played tracks.  The oldest track comes first."""
+    recently played tracks.  The oldest track comes first.
+
+    See disorder_protocol(5) for the meanings of the keys.  All keys are
+    plain strings but the values will be unicode strings."""
     return self._somequeue("recent")
 
   def queue(self):
     """Return the current queue.
 
     The return value is a list of dictionaries corresponding to
-    recently played tracks.  The next track to be played comes first."""
+    recently played tracks.  The next track to be played comes first.
+
+    See disorder_protocol(5) for the meanings of the keys.  All keys are
+    plain strings but the values will be unicode strings."""
     return self._somequeue("queue")
 
   def _somedir(self, command, dir, re):
@@ -582,10 +603,13 @@ class client:
     track -- the track to query
     key -- the preference to remove
 
-    The return value is the preference 
+    The return value is the preference.
     """
     ret, details = self._simple("get", track, key)
-    return details
+    if ret == 555:
+      return None
+    else:
+      return details
 
   def prefs(self, track):
     """Get all the preferences for a track.
@@ -656,6 +680,14 @@ class client:
     self._simple("search", _quote(words))
     return self._body()
 
+  def tags(self):
+    """List all tags
+
+    The return value is a list of all tags which apply to at least one
+    track."""
+    self._simple("tags")
+    return self._body()
+
   def stats(self):
     """Get server statistics.
 
@@ -699,6 +731,21 @@ class client:
     ret, details = self._simple("move", track, str(delta))
     return int(details)
 
+  def moveafter(self, target, tracks):
+    """Move a track in the queue
+
+    Arguments:
+    target -- target ID or None
+    tracks -- a list of IDs to move
+
+    If target is '' or is not in the queue then the tracks are moved to
+    the head of the queue.
+
+    Otherwise the tracks are moved to just after the target."""
+    if target is None:
+      target = ''
+    self._simple("moveafter", target, *tracks)
+
   def log(self, callback):
     """Read event log entries as they happen.
 
@@ -755,6 +802,37 @@ class client:
     ret, details = self._simple("part", track, context, part)
     return details
 
+  def setglobal(self, key, value):
+    """Set a global preference value.
+
+    Arguments:
+    key -- the preference name
+    value -- the new preference value
+    """
+    self._simple("set-global", key, value)
+
+  def unsetglobal(self, key):
+    """Unset a global preference value.
+
+    Arguments:
+    key -- the preference to remove
+    """
+    self._simple("set-global", key, value)
+
+  def getglobal(self, key):
+    """Get a global preference value.
+
+    Arguments:
+    key -- the preference to look up
+
+    The return value is the preference 
+    """
+    ret, details = self._simple("get-global", key)
+    if ret == 555:
+      return None
+    else:
+      return details
+
   ########################################################################
   # I/O infrastructure
 
@@ -809,7 +887,7 @@ class client:
     #
     # If an I/O error occurs, disconnect from the server.
     #
-    # On success returns response as a (code, details) tuple
+    # On success or 'normal' errors returns response as a (code, details) tuple
     #
     # On error raise operationError
     if self.state == 'disconnected':
@@ -819,7 +897,7 @@ class client:
     else:
       cmd = None
     res, details = self._response()
-    if res / 100 == 2:
+    if res / 100 == 2 or res == 555:
       return res, details
     raise operationError(res, details, cmd)
 
index 8f1a6668218f24e7c1fe916aa2a15c09d16ebdf1..fd3a35bbcf25407dd241f4df9e228bd67aaf3088 100644 (file)
@@ -20,4 +20,5 @@
 
 dist_pkgdata_DATA=completion.bash
 
-EXTRA_DIST=htmlman sedfiles.make text2c oggrename make-unidata
+EXTRA_DIST=htmlman sedfiles.make text2c oggrename make-unidata \
+       format-gcov-report make-version-string
index 1855aa88553f8a9d19fa0197e9947f696d20a80f..ec523a49e0c88804f3a56882a6ccbaebd3840c52 100755 (executable)
@@ -29,11 +29,12 @@ cp $d.tar.bz2 $HOME/work/web/disorder
 cp CHANGES $HOME/work/web/disorder/CHANGES.txt
 cp README $HOME/work/web/disorder/README.txt
 cp ChangeLog.d/*--* $HOME/work/web/disorder/ChangeLog.d
+bzr log > $HOME/work/web/disorder/ChangeLog.d/bzr-changelog.txt
 cd doc
 for f in *.[1-9].html; do
   echo $f
   rm -f $HOME/work/web/disorder/$f
   sed < $f > $HOME/work/web/disorder/$f 's/^@.*//'
 done
-cp doc/plumbing.svg $HOME/work/web/disorder
-cp doc/plumbing.png $HOME/work/web/disorder
+cp plumbing.svg $HOME/work/web/disorder
+cp plumbing.png $HOME/work/web/disorder
diff --git a/scripts/format-gcov-report b/scripts/format-gcov-report
new file mode 100755 (executable)
index 0000000..0369ab6
--- /dev/null
@@ -0,0 +1,113 @@
+#! /usr/bin/env python
+#
+# This file is part of DisOrder.
+# Copyright (C) 2007 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+import re,sys,os,string
+
+def fatal(msg):
+  sys.stderr.write("%s\n" % msg)
+  sys.exit(1)
+
+def sgmlquotechar(c):
+  if c == '&' or c == '<' or ord(c) < 32 or ord(c) > 126:
+    return "&#%d;" % ord(c)
+  else:
+    return c
+
+def sgmlquote(s):
+  return string.join(map(sgmlquotechar, s),'')
+
+missing = {}
+percent = {}
+total_lines = 0
+covered_lines = 0
+args = sys.argv[1:]
+htmldir = None
+while len(args) > 0 and re.match("^--", args[0]):
+  if args[0] == "--html":
+    htmldir = args[1]
+    args = args[2:]
+  else:
+    fatal("unknown option '%s'" % args[0])
+for s in args:
+  missing[s] = True
+
+name = None
+for line in sys.stdin:
+  line = line[:-1]
+  r = re.match("File ['`](?:.*/)?([^/]+.c)'", line)
+  if r:
+    name = r.group(1)
+    if name in missing:
+      del missing[name]
+  r = re.match("Lines executed:([0-9\\.]+)% of ([0-9]+)", line)
+  if r:
+    if name:
+      this_pc = float(r.group(1))
+      this_lines = int(r.group(2))
+      percent[name] = this_pc
+      total_lines += this_lines
+      covered_lines += this_lines * this_pc / 100.0
+    name = None
+
+def cmp(a,b):
+  if percent[a] < percent[b]: return -1 
+  elif percent[a] > percent[b]: return 1
+  else: return 0
+
+keys = percent.keys()
+keys.sort(cmp)
+
+if len(keys):
+  for k in keys:
+    print "%20s: %d%%" % (k, percent[k])
+  print "Total coverage: %d%%" % (100 * (covered_lines / total_lines))
+
+if htmldir is not None and len(keys):
+  index = open(os.path.join(htmldir, "index.html"), "w")
+  index.write("<html><head><title>gcov report</title>\n")
+  index.write("<body><h1>gcov report</h1>\n")
+  index.write("<table><tr><th>File</th><th>Coverage</th></tr>\n")
+  for k in keys:
+    index.write("<tr><td><a href=\"%s.html\">%s</a><td>%d%%\n" %
+                (sgmlquote(k), sgmlquote(k), percent[k]))
+  index.write("</table>\n")
+  index.write("<p>Total coverage: %d%%</p>\n" % (100 * (covered_lines / total_lines)))
+  missing_files = missing.keys()
+  missing_files.sort()
+  if len(missing_files) > 0:
+    index.write("<p>Missing files:</p>\n")
+    index.write("<ul>\n")
+    for mf in missing_files:
+      index.write("<li><a href=\"%s\">%s</a></li>\n" % (mf, mf))
+    index.write("</lu>\n")
+  for k in keys:
+    html = open(os.path.join(htmldir, "%s.html" % k), "w")
+    html.write("<html><head><title>%s</title>\n" % sgmlquote(k))
+    html.write("<body><h1>%s</h1>\n" % sgmlquote(k))
+    html.write("<pre>")
+    r = re.compile("^ *#####:.*")
+    for line in open("%s.gcov" % k, "r"):
+      if len(line) > 0 and line[-1] == '\n':
+        line = line[:-1]
+      if r.match(line):
+        html.write("<span style='background-color:#ffff00'>%s</span>\n" % sgmlquote(line))
+      else:
+        html.write("%s\n" % sgmlquote(line))
+    html.write("</pre>\n")
diff --git a/scripts/make-version-string b/scripts/make-version-string
new file mode 100755 (executable)
index 0000000..210e84f
--- /dev/null
@@ -0,0 +1,31 @@
+#! /usr/bin/perl -w
+use strict;
+sub output {
+    print @_ or die "$0: stdout: $!\n";
+}
+my @csv = `../config.status --version`;
+my $version;
+my $options;
+my $cc;
+my $compiler;
+for(@csv) {
+    chomp;
+    if(/disorder config\.status (\S+)/) {
+       $version = $1;
+    }
+    if(/with options \"(.*)\"/) {
+       $options = $1;
+    }
+}
+if(exists $ENV{CC}) {
+    $cc = $ENV{CC};
+    $cc =~ s/\s+/ /g;
+    my @cv = `$cc --version`;
+    $compiler = $cv[0];
+}
+die "no version found\n" unless defined $version;
+output("DisOrder $version\n");
+output(" configure options: $options\n") if defined $options;
+output(" compiler: $cc\n") if defined $compiler;
+output(" version: $compiler\n") if defined $cc;
+close STDOUT or die "$0: stdout: $!\n";
index 46b2276efc32a06eb6506c75108f1b30cfc6d962..95d2181ad13d5df51dfb63163acd25531503f8b6 100755 (executable)
@@ -1,7 +1,12 @@
 #! /usr/bin/perl -w
+my $class = "static";
+if($ARGV[0] eq '-extern') {
+    $class = "";
+    shift;
+}
 my $name = shift;
 push(@out, "/* autogenerated file, do not edit */\n\n");
-push(@out, "static const char $name\[] = \n");
+push(@out, "$class const char $name\[] = \n");
 while(<>) {
     next if /arch-tag/;
     s/[\\\"\?]/\\$&/g;
index d1f2fd716541d0e9f72db1159149848bf7bf5810..eae90c60e49ed976020a07dae5e0252994ec70bf 100644 (file)
@@ -108,15 +108,44 @@ trackname_DEPENDENCIES=../lib/libdisorder.a
 install-exec-hook:
        $(LIBTOOL) --mode=finish $(DESTDIR)$(libdir)
 
-check: check-help
+check: check-help check-decode
 
-# check everything has working --help
+# check everything has working --help and --version
 check-help: all
        ./disorderd --help > /dev/null
+       ./disorderd --version > /dev/null
        ./disorder-dump --help > /dev/null
+       ./disorder-dump --version > /dev/null
        ./disorder-deadlock --help > /dev/null
+       ./disorder-deadlock --version > /dev/null
        ./trackname --help > /dev/null
+       ./trackname --version > /dev/null
        ./disorder-speaker --help > /dev/null
+       ./disorder-speaker --version > /dev/null
+       ./disorder-decode --help > /dev/null
+       ./disorder-decode --version > /dev/null
+       ./disorder-normalize --help > /dev/null
+       ./disorder-normalize --version > /dev/null
+       ./disorder-stats --help > /dev/null
+       ./disorder-stats --version > /dev/null
+       ./disorder-dbupgrade --help > /dev/null
+       ./disorder-dbupgrade --version > /dev/null
+       ./disorder-rescan --help > /dev/null
+       ./disorder-rescan --version > /dev/null
+
+# My sox doesn't know MP3 or FLAC unfortunately
+check-decode: disorder-decode disorder-normalize
+       echo "speaker_backend network" > config
+       echo "broadcast 127.255.255.255 discard" > config
+       ./disorder-decode ${top_srcdir}/sounds/scratch.ogg | \
+         ./disorder-normalize --config config > decoded.raw
+       oggdec -b 16 -e 1 -R -s 1 -o oggdec.raw ${top_srcdir}/sounds/scratch.ogg
+       cmp decoded.raw oggdec.raw
+       sox ${top_srcdir}/sounds/scratch.ogg scratch.wav
+       ./disorder-decode scratch.wav | \
+         ./disorder-normalize --config config > decoded.raw
+       cmp decoded.raw oggdec.raw
+       rm -f scratch.wav config decoded.raw oggdec.raw
 
 cgi.o: ../lib/definitions.h
 
index b856ae5eb36ba312915b5afde9f218da4b27ca93..b4cf58e50d45a891fc80b4478409d8d862f3f945 100644 (file)
@@ -86,7 +86,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorder-dbupgrade version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
index 07e1825e705f409e9cb1f211c856a010c9725219..6e71ef040184b8df6ef99d6d77f3629382a9131e 100644 (file)
@@ -446,7 +446,7 @@ static void exp_version(int attribute((unused)) nargs,
                        char attribute((unused)) **args,
                        cgi_sink *output,
                        void attribute((unused)) *u) {
-  cgi_output(output, "%s", disorder_version_string);
+  cgi_output(output, "%s", disorder_short_version_string);
 }
 
 static void exp_nonce(int attribute((unused)) nargs,
index e45694fc1ada3f0efd0a6a9dfac2971847514beb..bce3eee6037e3fadeecf2acbb5a056cdb0ff52cd 100644 (file)
@@ -72,7 +72,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorder-deadlock version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
index 47227a29ecb55ef375f3611395cb0f5b1fb053aa..639ad159cd2c8d607286d9378d4de37ff9f33ccf 100644 (file)
@@ -440,7 +440,7 @@ static void help(void) {
 
 /* Display version number and terminate. */
 static void version(void) {
-  xprintf("disorder-decode version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
index 758907cbd0ac4370fe99ad9e6729191ded725f43..ba18c237ff7c44a2100ec45c331a2f893234ef66 100644 (file)
@@ -71,7 +71,6 @@ static const struct option options[] = {
   { "foreground", no_argument, 0, 'f' },
   { "log", required_argument, 0, 'l' },
   { "pidfile", required_argument, 0, 'P' },
-  { "no-initial-rescan", no_argument, 0, 'N' },
   { "wide-open", no_argument, 0, 'w' },
   { "syslog", no_argument, 0, 's' },
   { 0, 0, 0, 0 }
@@ -95,7 +94,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorderd version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
@@ -207,7 +206,6 @@ static void fix_path(void) {
 int main(int argc, char **argv) {
   int n, background = 1, logsyslog = 0;
   const char *pidfile = 0;
-  int initial_rescan = 1;
 
   set_progname(argv);
   mem_init();
@@ -223,7 +221,6 @@ int main(int argc, char **argv) {
     case 'd': debugging = 1; break;
     case 'f': background = 0; break;
     case 'P': pidfile = optarg; break;
-    case 'N': initial_rescan = 0; break;
     case 's': logsyslog = 1; break;
     case 'w': wideopen = 1; break;
     default: fatal(0, "invalid option");
@@ -288,8 +285,8 @@ int main(int argc, char **argv) {
   if(ev_signal(ev, SIGTERM, handle_sigterm, 0)) fatal(0, "ev_signal failed");
   /* ignore SIGPIPE */
   signal(SIGPIPE, SIG_IGN);
-  /* start a rescan straight away */
-  if(initial_rescan) {
+  /* start a rescan straight away if this is a new installation */
+  if(!trackdb_existing_database) {
     trackdb_rescan(0/*ev*/);
     /* No ev -> the rescan will block.  Since we called reconfigure() already
      * any clients will also be forced to block. */
index 84b45d046199f1c28cd43391e02eba6cd1bb1daa..119875f55fee3a8a19ac8eafb928cf0778257126 100644 (file)
@@ -82,7 +82,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorder-dump version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
@@ -106,6 +106,7 @@ static void do_dump(FILE *fp, const char *tag,
       fatal(errno, "error calling ftruncate");
     if(fprintf(fp, "V%c\n", (tracksdb || searchdb) ? '1' : '0') < 0)
       fatal(errno, "error writing to %s", tag);
+    /* dump the preferences */
     cursor = trackdb_opencursor(trackdb_prefsdb, tid);
     err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
                         DB_FIRST);
@@ -122,6 +123,23 @@ static void do_dump(FILE *fp, const char *tag,
     if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
     cursor = 0;
 
+    /* dump the global preferences */
+    cursor = trackdb_opencursor(trackdb_globaldb, tid);
+    err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                        DB_FIRST);
+    while(err == 0) {
+      if(fputc('G', fp) < 0
+         || urlencode(s, k.data, k.size)
+         || fputc('\n', fp) < 0
+         || urlencode(s, d.data, d.size)
+         || fputc('\n', fp) < 0)
+        fatal(errno, "error writing to %s", tag);
+      err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
+                          DB_NEXT);
+    }
+    if(trackdb_closecursor(cursor)) { cursor = 0; goto fail; }
+    cursor = 0;
+    
     if(tracksdb) {
       cursor = trackdb_opencursor(trackdb_tracksdb, tid);
       err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
@@ -256,12 +274,16 @@ static int undump_dbt(FILE *fp, const char *tag, DBT *dbt) {
 static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) {
   int err, c;
   DBT k, d;
+  const char *which_name;
+  DB *which_db;
 
   info("undumping");
   if(fseek(fp, 0, SEEK_SET) < 0)
     fatal(errno, "error calling fseek on %s", tag);
   if((err = truncdb(tid, trackdb_prefsdb))) return err;
+  if((err = truncdb(tid, trackdb_globaldb))) return err;
   if((err = truncdb(tid, trackdb_searchdb))) return err;
+  if((err = truncdb(tid, trackdb_tagsdb))) return err;
   c = getc(fp);
   while(!ferror(fp) && !feof(fp)) {
     switch(c) {
@@ -273,17 +295,25 @@ static int undump_from_fp(DB_TXN *tid, FILE *fp, const char *tag) {
     case 'E':
       return 0;
     case 'P':
+    case 'G':
+      if(c == 'P') {
+       which_db = trackdb_prefsdb;
+       which_name = "prefs.db";
+      } else {
+       which_db = trackdb_globaldb;
+       which_name = "global.db";
+      }
       if(undump_dbt(fp, tag, prepare_data(&k))
          || undump_dbt(fp, tag, prepare_data(&d)))
         break;
-      switch(err = trackdb_prefsdb->put(trackdb_prefsdb, tid, &k, &d, 0)) {
+      switch(err = trackdb_prefsdb->put(which_db, tid, &k, &d, 0)) {
       case 0:
         break;
       case DB_LOCK_DEADLOCK:
-        error(0, "error updating prefs.db: %s", db_strerror(err));
+        error(0, "error updating %s: %s", which_name, db_strerror(err));
         return err;
       default:
-        fatal(0, "error updating prefs.db: %s", db_strerror(err));
+        fatal(0, "error updating %s: %s", which_name, db_strerror(err));
       }
       break;
     case 'T':
index 085d4dfb126cc3ff06e9b5f61d4f2417b071a44d..87b0f3d7bc5c356419d60e4f4d6e5a391ca5b465 100644 (file)
@@ -73,7 +73,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorder-normalize version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
index bd27474ce40a17ebd3ca23e4a672b4a25cbdfb21..7b7079994fbb2a2a3e88f80bf6b0790fb85ec5af 100644 (file)
@@ -312,14 +312,16 @@ static int start(ev_source *ev,
 
   memset(&sm, 0, sizeof sm);
   D(("start %s %d", q->id, prepare_only));
-  if(find_player_pid(q->id) > 0) {
-    if(prepare_only) return START_OK;
-    /* We have already prepared this track so we just need to tell the speaker
-     * process to start actually playing the queued up audio data */
-    strcpy(sm.id, q->id);
-    sm.type = SM_PLAY;
-    speaker_send(speaker_fd, &sm);
-    D(("sent SM_PLAY for %s", sm.id));
+  if(q->prepared) {
+    /* The track is alraedy prepared */
+    if(!prepare_only) {
+      /* We want to run it, since it's prepared the answer is to tell the
+       * speaker to set it off */
+      strcpy(sm.id, q->id);
+      sm.type = SM_PLAY;
+      speaker_send(speaker_fd, &sm);
+      D(("sent SM_PLAY for %s", sm.id));
+    }
     return START_OK;
   }
   /* Find the player plugin. */
@@ -478,6 +480,7 @@ static int start(ev_source *ev,
     return START_SOFTFAIL;
   }
   store_player_pid(q->id, pid);
+  q->prepared = 1;
   if(lfd != -1)
     xclose(lfd);
   setpgid(pid, pid);
index b878929a82adc97812030d3a898322ec322ac3f8..e45eaa61128eb0cd5aee37a31454e1613dc302d3 100644 (file)
@@ -84,7 +84,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorder-rescan version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
index 2b0e89e8f51fbae93c3f219094207de2f4a5a6a5..71262886ec1b1d123605961616eb523ac4cb2a57 100644 (file)
 #include "disorder.h"
 
 /* the head of the queue is played next, so normally we add to the tail */
-struct queue_entry qhead = { &qhead, &qhead, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+struct queue_entry qhead = { &qhead, &qhead, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
 
 /* the head of the recent list is the oldest thing, the tail the most recently
  * played */
-struct queue_entry phead = { &phead, &phead, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+struct queue_entry phead = { &phead, &phead, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
 
 static long pcount;
 
index c8b4a622d0238e6d5d255d088209d86e28b1677d..a36996409331d7105d5d5adf99452675b491eb7d 100644 (file)
@@ -225,7 +225,7 @@ static int c_play(struct conn *c, char **vec,
    * anything. */
   if(q == qhead.next && playing)
     prepare(c->ev, q);
-  sink_writes(ev_writer_sink(c->w), "250 queued\n");
+  sink_printf(ev_writer_sink(c->w), "252 %s\n", q->id);
   /* If the queue was empty but we are for some reason paused then
    * unpause. */
   if(!playing) resume_playing(0);
@@ -346,7 +346,7 @@ static int c_version(struct conn *c,
                     char attribute((unused)) **vec,
                     int attribute((unused)) nvec) {
   /* VERSION had better only use the basic character set */
-  sink_printf(ev_writer_sink(c->w), "251 %s\n", disorder_version_string);
+  sink_printf(ev_writer_sink(c->w), "251 %s\n", disorder_short_version_string);
   return 1;                    /* completed */
 }
 
@@ -580,7 +580,7 @@ static int c_get(struct conn *c,
   if(vec[1][0] != '_' && (v = trackdb_get(vec[0], vec[1])))
     sink_printf(ev_writer_sink(c->w), "252 %s\n", v);
   else
-    sink_writes(ev_writer_sink(c->w), "550 not found\n");
+    sink_writes(ev_writer_sink(c->w), "555 not found\n");
   return 1;
 }
 
@@ -924,7 +924,7 @@ static int c_get_global(struct conn *c,
   if(s)
     sink_printf(ev_writer_sink(c->w), "252 %s\n", s);
   else
-    sink_writes(ev_writer_sink(c->w), "550 not found\n");
+    sink_writes(ev_writer_sink(c->w), "555 not found\n");
   return 1;
 }
 
index 17efb99718c666e8e089113e94e3c3705c3ed17e..27b26647a0e9c61aa4f92db7328c80d06be52ef9 100644 (file)
@@ -1,6 +1,7 @@
 /*
  * This file is part of DisOrder
  * Copyright (C) 2005, 2006, 2007 Richard Kettlewell
+ * Portions (C) 2007 Mark Wooding
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 40abca59f14aeeba85f4eee8ad9dfa32a112a379..810aff766ba73c2041bd5b7b8efdae469beabc7d 100644 (file)
@@ -125,6 +125,9 @@ static void network_init(void) {
       const int mttl = config->multicast_ttl;
       if(setsockopt(bfd, IPPROTO_IP, IP_MULTICAST_TTL, &mttl, sizeof mttl) < 0)
         fatal(errno, "error setting IP_MULTICAST_TTL on multicast socket");
+      if(setsockopt(bfd, IPPROTO_IP, IP_MULTICAST_LOOP,
+                    &config->multicast_loop, sizeof one) < 0)
+        fatal(errno, "error setting IP_MULTICAST_LOOP on multicast socket");
       break;
     }
     case PF_INET6: {
@@ -132,6 +135,9 @@ static void network_init(void) {
       if(setsockopt(bfd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS,
                     &mttl, sizeof mttl) < 0)
         fatal(errno, "error setting IPV6_MULTICAST_HOPS on multicast socket");
+      if(setsockopt(bfd, IPPROTO_IP, IPV6_MULTICAST_LOOP,
+                    &config->multicast_loop, sizeof (int)) < 0)
+        fatal(errno, "error setting IPV6_MULTICAST_LOOP on multicast socket");
       break;
     }
     default:
@@ -213,24 +219,16 @@ static size_t network_play(size_t frames) {
      * the value we deduce from time comparison.
      *
      * Suppose we have 1s track started at t=0, and another track begins to
-     * play at t=2s.  Suppose RTP_AHEAD_MS=1000 and 44100Hz stereo.  In that
-     * case we'll send 1s of audio as fast as we can, giving rtp_time=88200.
-     * rtp_time stops at this point.
+     * play at t=2s.  Suppose 44100Hz stereo.  We send 1s of audio over the
+     * next (about) one second, giving rtp_time=88200.  rtp_time stops at this
+     * point.
      *
      * At t=2s we'll have calculated target_rtp_time=176400.  In this case we
      * set rtp_time=176400 and the player can correctly conclude that it
      * should leave 1s between the tracks.
      *
-     * Suppose instead that the second track arrives at t=0.5s, and that
-     * we've managed to transmit the whole of the first track already.  We'll
-     * have target_rtp_time=44100.
-     *
-     * The desired behaviour is to play the second track back to back with
-     * first.  In this case therefore we do not modify rtp_time.
-     *
-     * Is it ever right to reduce rtp_time?  No; for that would imply
-     * transmitting packets with overlapping timestamp ranges, which does not
-     * make sense.
+     * It's never right to reduce rtp_time, for that would imply packets with
+     * overlapping timestamp ranges, which does not make sense.
      */
     target_rtp_time &= ~(uint64_t)1;    /* stereo! */
     if(target_rtp_time > rtp_time) {
@@ -240,15 +238,8 @@ static size_t network_play(size_t frames) {
            target_rtp_time - rtp_time);
       rtp_time = target_rtp_time;
     } else if(target_rtp_time < rtp_time) {
-      const int64_t samples_ahead = ((uint64_t)RTP_AHEAD_MS
-                                     * config->sample_format.rate
-                                     * config->sample_format.channels
-                                     / 1000);
-        
-      if(target_rtp_time + samples_ahead < rtp_time) {
-        info("reversing rtp_time by %"PRIu64" samples",
-             rtp_time - target_rtp_time);
-      }
+      info("would reverse rtp_time by %"PRIu64" samples",
+           rtp_time - target_rtp_time);
     }
   }
   header.vpxcc = 2 << 6;              /* V=2, P=0, X=0, CC=0 */
@@ -305,30 +296,27 @@ static void network_beforepoll(int *timeoutp) {
   uint64_t target_rtp_time;
   const int64_t samples_per_second = config->sample_format.rate
                                    * config->sample_format.channels;
-  const int64_t samples_ahead = ((uint64_t)RTP_AHEAD_MS
-                                 * samples_per_second
-                                 / 1000);
   int64_t lead, ahead_ms;
   
   /* If we're starting then initialize the base time */
   if(!rtp_time)
     xgettimeofday(&rtp_time_0, 0);
-  /* We send audio data whenever we get RTP_AHEAD seconds or more
-   * behind */
+  /* We send audio data whenever we would otherwise get behind */
   xgettimeofday(&now, 0);
   target_us = tvsub_us(now, rtp_time_0);
   assert(target_us <= UINT64_MAX / 88200);
   target_rtp_time = (target_us * config->sample_format.rate
                                * config->sample_format.channels)
                      / 1000000;
+  /* Lead is how far ahead we are */
   lead = rtp_time - target_rtp_time;
-  if(lead < samples_ahead)
-    /* We've not reached the desired lead, write as fast as we can */
+  if(lead <= 0)
+    /* We're behind or even, so we'll need to write as soon as we can */
     bfd_slot = addfd(bfd, POLLOUT);
   else {
-    /* We've reached the desired lead, we can afford to wait a bit even if the
-     * IP stack thinks it can accept more. */
-    ahead_ms = 1000 * (lead - samples_ahead) / samples_per_second;
+    /* We've ahead, we can afford to wait a bit even if the IP stack thinks it
+     * can accept more. */
+    ahead_ms = 1000 * lead / samples_per_second;
     if(ahead_ms < *timeoutp)
       *timeoutp = ahead_ms;
   }
index 9682cdee6d7c3ccb458cac4befa38e120ef1e63d..69322ff73157da1f3e383518d5dd04d2660e0db9 100644 (file)
@@ -1,6 +1,7 @@
 /*
  * This file is part of DisOrder
  * Copyright (C) 2007 Richard Kettlewell
+ * Portions copyright (C) 2007 Ross Younger
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
index 55dde7ee9bbf2572da6e317bf9e2ecfb378982cb..d21ad7cd8c4e976eff06580ab2e20de92896c9c6 100644 (file)
@@ -1,6 +1,7 @@
 /*
  * This file is part of DisOrder
  * Copyright (C) 2005, 2006, 2007 Richard Kettlewell
+ * Portions (C) 2007 Mark Wooding
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -141,7 +142,7 @@ static void help(void) {
 
 /* Display version number and terminate. */
 static void version(void) {
-  xprintf("disorder-speaker version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
@@ -195,7 +196,7 @@ static void destroy(struct track *t) {
  * main loop whenever the track's file descriptor is readable, assuming the
  * buffer has not reached the maximum allowed occupancy.
  */
-static int fill(struct track *t) {
+static int speaker_fill(struct track *t) {
   size_t where, left;
   int n;
 
@@ -218,9 +219,12 @@ static int fill(struct track *t) {
     if(n == 0) {
       D(("fill %s: eof detected", t->id));
       t->eof = 1;
+      t->playable = 1;
       return -1;
     }
     t->used += n;
+    if(t->used == sizeof t->buffer)
+      t->playable = 1;
   }
   return 0;
 }
@@ -280,6 +284,17 @@ static void maybe_finished(void) {
     abandon();
 }
 
+/** @brief Return nonzero if we want to play some audio
+ *
+ * We want to play audio if there is a current track; and it is not paused; and
+ * it is playable according to the rules for @ref track::playable.
+ */
+static int playable(void) {
+  return playing
+         && !paused
+         && playing->playable;
+}
+
 /** @brief Play up to @p frames frames of audio
  *
  * It is always safe to call this function.
@@ -290,15 +305,15 @@ static void maybe_finished(void) {
  * - If there is not enough audio to play then it play what is available.
  *
  * If there are not enough frames to play then whatever is available is played
- * instead.  It is up to mainloop() to ensure that play() is not called when
- * unreasonably only an small amounts of data is available to play.
+ * instead.  It is up to mainloop() to ensure that speaker_play() is not called
+ * when unreasonably only an small amounts of data is available to play.
  */
-static void play(size_t frames) {
+static void speaker_play(size_t frames) {
   size_t avail_frames, avail_bytes, written_frames;
   ssize_t written_bytes;
 
-  /* Make sure there's a track to play and it is not pasued */
-  if(!playing || paused)
+  /* Make sure there's a track to play and it is not paused */
+  if(!playable())
     return;
   /* Make sure the output device is open */
   if(device_state != device_open) {
@@ -336,6 +351,11 @@ static void play(size_t frames) {
    * empty) wrap it back to the start. */
   if(!playing->used || playing->start == (sizeof playing->buffer))
     playing->start = 0;
+  /* If the buffer emptied out mark the track as unplayably */
+  if(!playing->used && !playing->eof) {
+    error(0, "track buffer emptied");
+    playing->playable = 0;
+  }
   frames -= written_frames;
   return;
 }
@@ -389,18 +409,6 @@ static const struct speaker_backend *backends[] = {
   0
 };
 
-/** @brief Return nonzero if we want to play some audio
- *
- * We want to play audio if there is a current track; and it is not paused; and
- * there are at least @ref FRAMES frames of audio to play, or we are in sight
- * of the end of the current track.
- */
-static int playable(void) {
-  return playing
-         && !paused
-         && (playing->used >= FRAMES || playing->eof);
-}
-
 /** @brief Main event loop */
 static void mainloop(void) {
   struct track *t;
@@ -460,15 +468,15 @@ static void mainloop(void) {
       /* We want to play some audio */
       if(device_state == device_open) {
         if(backend->ready())
-          play(3 * FRAMES);
+          speaker_play(3 * FRAMES);
       } else {
         /* We must be in _closed or _error, and it should be the latter, but we
          * cope with either.
          *
-         * We most likely timed out, so now is a good time to retry.  play()
-         * knows to re-activate the device if necessary.
+         * We most likely timed out, so now is a good time to retry.
+         * speaker_play() knows to re-activate the device if necessary.
          */
-        play(3 * FRAMES);
+        speaker_play(3 * FRAMES);
       }
     }
     /* Perhaps a connection has arrived */
@@ -495,7 +503,7 @@ static void mainloop(void) {
           t = findtrack(id, 1/*create*/);
           write(fd, "", 1);             /* write an ack */
           if(t->fd != -1) {
-            error(0, "got a connection for a track that already has one");
+            error(0, "%s: already got a connection", id);
             xclose(fd);
           } else {
             nonblock(fd);
@@ -522,9 +530,9 @@ static void mainloop(void) {
             error(0, "cannot play track because no connection arrived");
           playing = t;
           /* We attempt to play straight away rather than going round the loop.
-           * play() is clever enough to perform any activation that is
+           * speaker_play() is clever enough to perform any activation that is
            * required. */
-          play(3 * FRAMES);
+          speaker_play(3 * FRAMES);
           report();
          break;
        case SM_PAUSE:
@@ -538,7 +546,7 @@ static void mainloop(void) {
             paused = 0;
             /* As for SM_PLAY we attempt to play straight away. */
             if(playing)
-              play(3 * FRAMES);
+              speaker_play(3 * FRAMES);
           }
           report();
          break;
@@ -571,7 +579,7 @@ static void mainloop(void) {
       if(t->fd != -1
          && t->slot != -1
          && (fds[t->slot].revents & (POLLIN | POLLHUP)))
-         fill(t);
+         speaker_fill(t);
     /* Maybe we finished playing a track somewhere in the above */
     maybe_finished();
     /* If we don't need the sound device for now then close it for the benefit
index de89ee0476384c612baadc51ee8bd8b84823b7a1..2786735c6d80c9e5a4e853496e90a9843e6a90a4 100644 (file)
@@ -55,9 +55,6 @@
  */
 #define NETWORK_BYTES (1500-8/*UDP*/-40/*IP*/-8/*conservatism*/)
 
-/** @brief Maximum RTP playahead (ms) */
-#define RTP_AHEAD_MS 100
-
 /** @brief Maximum number of FDs to poll for */
 #define NFDS 256
 
@@ -91,6 +88,14 @@ struct track {
   /** @brief Slot in @ref fds */
   int slot;
 
+  /** @brief Set when playable
+   *
+   * A track becomes playable whenever it fills its buffer or reaches EOF; it
+   * stops being playable when it entirely empties its buffer.  Tracks start
+   * out life not playable.
+   */
+  int playable;
+  
   /** @brief Input buffer
    *
    * 1Mbyte is enough for nearly 6s of 44100Hz 16-bit stereo
index 66760f0963f1353a9db68162763c3eb48462b441..79cae43bbf2f4b957667c6488ae0b3343437276c 100644 (file)
@@ -57,7 +57,7 @@ void quit(ev_source *ev) {
   trackdb_close();
   trackdb_deinit();
   info("terminating");
-  _exit(0);
+  exit(0);
 }
 
 static void reset_socket(ev_source *ev) {
index a12b8f66641065fb7eb880e5370064f10c4e0fdc..c397be8a10a4530ff912bcf9dc83ba4b932d4350 100644 (file)
@@ -65,7 +65,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorder-stats version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
index bc526caad6acff31d0e952c028df74d0f75f0035..d1878c01fa4f0ac3b98330f255b71cf3176271ab 100644 (file)
@@ -71,10 +71,14 @@ static char **trackdb_new_tid(int *ntracksp,
                               int maxtracks,
                               DB_TXN *tid);
 static int trackdb_expire_noticed_tid(time_t earliest, DB_TXN *tid);
+static char *normalize_tag(const char *s, size_t ns);
 
 const struct cache_type cache_files_type = { 86400 };
 unsigned long cache_files_hits, cache_files_misses;
 
+/** @brief Set by trackdb_open() */
+int trackdb_existing_database;
+
 /* setup and teardown ********************************************************/
 
 static const char *home;                /* home had better not change */
@@ -306,7 +310,7 @@ static DB *open_db(const char *path,
  * - @p TRACKDB_OPEN_FOR_UPGRADE, if this is disorder-dbupgrade
  */
 void trackdb_open(int flags) {
-  int newdb, err;
+  int err;
   pid_t pid;
 
   /* sanity checks */
@@ -359,14 +363,14 @@ void trackdb_open(int flags) {
       /* This doesn't make any sense */
       fatal(0, "database is already at current version");
     }
-    newdb = 0;
+    trackdb_existing_database = 1;
   } else {
     if(flags & TRACKDB_OPEN_FOR_UPGRADE) {
       /* Cannot upgrade a new database */
       fatal(0, "cannot upgrade a database that does not exist");
     }
     /* This is a brand new database */
-    newdb = 1;
+    trackdb_existing_database = 0;
   }
   /* open the databases */
   trackdb_tracksdb = open_db("tracks.db",
@@ -379,7 +383,7 @@ void trackdb_open(int flags) {
   trackdb_globaldb = open_db("global.db", 0, DB_HASH, DB_CREATE, 0666);
   trackdb_noticeddb = open_db("noticed.db",
                              DB_DUPSORT, DB_BTREE, DB_CREATE, 0666);
-  if(newdb) {
+  if(!trackdb_existing_database) {
     /* Stash the database version */
     char buf[32];
 
@@ -675,6 +679,40 @@ static void word_split(struct vector *v,
     vector_append(v, utf32_to_utf8(w32[i], utf32_len(w32[i]), 0));
 }
 
+/** @brief Normalize a tag
+ * @param s Tag
+ * @param ns Length of tag
+ * @return Normalized string or NULL on error
+ *
+ * The return value will be:
+ * - case-folded
+ * - have no leading or trailing space
+ * - have no combining characters
+ * - all spacing between words will be a single U+0020 SPACE
+ */
+static char *normalize_tag(const char *s, size_t ns) {
+  uint32_t *s32, **w32;
+  size_t ns32, nw32, i;
+  struct dynstr d[1];
+
+  if(!(s32 = utf8_to_utf32(s, ns, &ns32)))
+    return 0;
+  if(!(s32 = utf32_casefold_compat(s32, ns32, &ns32))) /* ->NFKD */
+    return 0;
+  ns32 = remove_combining_chars(s32, ns32);
+  /* Split into words, no Word_Break tailoring */
+  w32 = utf32_word_split(s32, ns32, &nw32, 0);
+  /* Compose back into a string */
+  dynstr_init(d);
+  for(i = 0; i < nw32; ++i) {
+    if(i)
+      dynstr_append(d, ' ');
+    dynstr_append_string(d, utf32_to_utf8(w32[i], utf32_len(w32[i]), 0));
+  }
+  dynstr_terminate(d);
+  return d->vec;
+}
+
 /* compute the words of a track name */
 static char **track_to_words(const char *track,
                              const struct kvp *p) {
@@ -740,7 +778,8 @@ static char **parsetags(const char *s) {
       /* strip trailing spaces */
       while(s > t && s[-1] == ' ')
         --s;
-      vector_append(&v, xstrndup(t, s - t));
+      /* add tag to list */
+      vector_append(&v, normalize_tag(t, (size_t)(s - t)));
       /* skip intermediate and trailing separators */
       while(*s && (!tagchar(*s) || *s == ' '))
         ++s;
@@ -1062,11 +1101,43 @@ static int get_stats(struct vector *v,
   return 0;
 }
 
+/** @brief One entry in the search league */
 struct search_entry {
   char *word;
   int n;
 };
 
+/** @brief Add a word to the search league
+ * @param se Pointer to search league
+ * @param count Maximum size for search league
+ * @param nse Current size of search league
+ * @param word New word, or NULL
+ * @param n How often @p word appears
+ * @return New size of search league
+ */
+static int register_search_entry(struct search_entry *se,
+                                 int count,
+                                 int nse,
+                                 char *word,
+                                 int n) {
+  int i;
+
+  if(word && (nse < count || n > se[nse - 1].n)) {
+    /* Find the starting point */
+    if(nse == count)
+      i = nse - 1;
+    else
+      i = nse++;
+    /* Find the insertion point */
+    while(i > 0 && n > se[i - 1].n)
+      --i;
+    memmove(&se[i + 1], &se[i], (nse - i - 1) * sizeof *se);
+    se[i].word = word;
+    se[i].n = n;
+  }
+  return nse;
+}
+
 /* find the top COUNT words in the search database */
 static int search_league(struct vector *v, int count, DB_TXN *tid) {
   struct search_entry *se;
@@ -1079,25 +1150,14 @@ static int search_league(struct vector *v, int count, DB_TXN *tid) {
 
   cursor = trackdb_opencursor(trackdb_searchdb, tid);
   se = xmalloc(count * sizeof *se);
+  /* Walk across the whole database counting up the number of times each
+   * word appears. */
   while(!(err = cursor->c_get(cursor, prepare_data(&k), prepare_data(&d),
                               DB_NEXT))) {
     if(word && wl == k.size && !strncmp(word, k.data, wl))
-      ++n;
+      ++n;                              /* same word again */
     else {
-#define FINALIZE() do {                                                \
-  if(word && (nse < count || n > se[nse - 1].n)) {             \
-    if(nse == count)                                           \
-      i = nse - 1;                                             \
-    else                                                       \
-      i = nse++;                                               \
-    while(i > 0 && n > se[i - 1].n)                            \
-      --i;                                                     \
-    memmove(&se[i + 1], &se[i], (nse - i) * sizeof *se);       \
-    se[i].word = word;                                         \
-    se[i].n = n;                                               \
-  }                                                            \
-} while(0)
-      FINALIZE();
+      nse = register_search_entry(se, count, nse, word, n);
       word = xstrndup(k.data, wl = k.size);
       n = 1;
     }
@@ -1114,7 +1174,7 @@ static int search_league(struct vector *v, int count, DB_TXN *tid) {
   }
   if(trackdb_closecursor(cursor)) err = DB_LOCK_DEADLOCK;
   if(err) return err;
-  FINALIZE();
+  nse = register_search_entry(se, count, nse, word, n);
   byte_xasprintf(&str, "Top %d search words:", nse);
   vector_append(v, str);
   for(i = 0; i < nse; ++i) {
@@ -1167,7 +1227,7 @@ struct stats_details {
 
 static void stats_complete(struct stats_details *d) {
   char *s;
-  
+
   if(!(d->exited && d->closed))
     return;
   byte_xasprintf(&s, "\n"
@@ -1823,6 +1883,7 @@ static const char *checktag(const char *s) {
 char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) {
   const char **w, *best = 0, *tag;
   char **twords, **tags;
+  char *istag;
   int i, j, n, err, what;
   DBC *cursor = 0;
   DBT k, d;
@@ -1836,22 +1897,30 @@ char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) {
   *ntracks = 0;                                /* for early returns */
   /* normalize all the words */
   w = xmalloc(nwordlist * sizeof (char *));
+  istag = xmalloc_noptr(nwordlist);
   for(n = 0; n < nwordlist; ++n) {
     uint32_t *w32;
     size_t nw32;
     
     w[n] = utf8_casefold_compat(wordlist[n], strlen(wordlist[n]), 0);
-    if(checktag(w[n])) ++ntags;         /* count up tags */
-    /* Strip out combining characters (AFTER checking whether it's a tag) */
-    if(!(w32 = utf8_to_utf32(w[n], strlen(w[n]), &nw32)))
-      return 0;
-    nw32 = remove_combining_chars(w32, nw32);
-    if(!(w[n] = utf32_to_utf8(w32, nw32, 0)))
-      return 0;
+    if(checktag(w[n])) {
+      ++ntags;         /* count up tags */
+      /* Normalize the tag */
+      w[n] = normalize_tag(w[n] + 4, strlen(w[n] + 4));
+      istag[n] = 1;
+    } else {
+      /* Normalize the search term by removing combining characters */
+      if(!(w32 = utf8_to_utf32(w[n], strlen(w[n]), &nw32)))
+        return 0;
+      nw32 = remove_combining_chars(w32, nw32);
+      if(!(w[n] = utf32_to_utf8(w32, nw32, 0)))
+        return 0;
+      istag[n] = 0;
+    }
   }
   /* find the longest non-stopword */
   for(n = 0; n < nwordlist; ++n)
-    if(!stopword(w[n]) && !checktag(w[n]))
+    if(!istag[n] && !stopword(w[n]))
       if(!best || strlen(w[n]) > strlen(best))
        best = w[n];
   /* TODO: we should at least in principal be able to identify the word or tag
@@ -1860,7 +1929,7 @@ char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) {
   if(ntags && !best) {
     /* Only tags are listed.  We limit to the first and narrow down with the
      * rest. */
-    best = checktag(w[0]);
+    best = istag[0] ? w[0] : 0;
     db = trackdb_tagsdb;
     dbname = "tags";
   } else if(best) {
@@ -1909,7 +1978,8 @@ char **trackdb_search(char **wordlist, int nwordlist, int *ntracks) {
       twords = track_to_words(v.vec[n], p);
       tags = parsetags(kvp_get(p, "tags"));
       for(i = 0; i < nwordlist; ++i) {
-        if((tag = checktag(w[i]))) {
+        if(istag[i]) {
+          tag = w[i];
           /* Track must have this tag */
           for(j = 0; tags[j]; ++j)
             if(!strcmp(tag, tags[j])) break; /* tag found */
index 3cce6a5cced1ab970fb0404a2f1fea40f620ca28..c6a5897577b2f59786001c66425d94e394dddb6f 100644 (file)
@@ -64,6 +64,8 @@ void trackdb_open(int flags);
 void trackdb_close(void);
 /* open/close track databases */
 
+extern int trackdb_existing_database;
+
 char **trackdb_stats(int *nstatsp);
 /* return a list of database stats */
 
index 1e0bb45589f9b81846cd435445340ecd462ca4f6..b14e9e5a12903d5f2e4a4a77eb7cd8492e51ce70 100644 (file)
@@ -57,7 +57,7 @@ static void help(void) {
 
 /* display version number and terminate */
 static void version(void) {
-  xprintf("disorder version %s\n", disorder_version_string);
+  xprintf("%s", disorder_version_string);
   xfclose(stdout);
   exit(0);
 }
index e79c02186d33a6c9e7fef70554e3dfaadb397bc8..2cd5e633cdfa328a9559c618db25341892e7c109 100644 (file)
 # USA
 #
 
+noinst_PROGRAMS=disorder-udplog
+
+AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
+
+disorder_udplog_SOURCES=udplog.c
+disorder_udplog_LDADD=$(LIBOBJS) ../lib/libdisorder.a
+disorder_udplog_DEPENDENCIES=../lib/libdisorder.a
+
 check:
        ${PYTHON} ${srcdir}/alltests
 
-EXTRA_DIST=alltests dtest.py nothing.py version.py dbversion.py search.py
-
+EXTRA_DIST=alltests dtest.py dbversion.py search.py \
+       queue.py dump.py play.py
index d7f58a59f6a5b3a8e63a5bb5d1724301544c9efb..35f71adae04172bac8194b655c0b837efc07e0d5 100755 (executable)
@@ -31,9 +31,15 @@ def test():
     dtest.stop_daemon()
     # Revert to default configuration
     dtest.copyfile(configsave, config)
-    print "Testing daemon manages to upgrade..."
+    print " testing daemon manages to upgrade..."
     dtest.start_daemon()
-    assert dtest.check_files() == 0
+    assert dtest.check_files() == 0, "dtest.check_files"
+    print " getting server version"
+    c = disorder.client()
+    v = c.version()
+    print "Server version: %s" % v
+    print " getting server stats"
+    s = c.stats()
 
 if __name__ == '__main__':
     dtest.run()
index 3936935b463d02ac4a606ade4154f99fd7b438b3..cd87c503788b9bdb58a40a2bc0ddd260658d33e9 100644 (file)
@@ -21,7 +21,7 @@
 
 """Utility module used by tests"""
 
-import os,os.path,subprocess,sys,re,time,unicodedata
+import os,os.path,subprocess,sys,re,time,unicodedata,random,socket
 
 def fatal(s):
     """Write an error message and exit"""
@@ -41,9 +41,11 @@ else:
 sys.path.insert(0, os.path.join(top_builddir, "python"))
 import disorder
 
-# Make sure the server build directory is on the executable search path
+# Make sure the build directories are on the executable search path
 ospath = os.environ["PATH"].split(os.pathsep)
 ospath.insert(0, os.path.join(top_builddir, "server"))
+ospath.insert(0, os.path.join(top_builddir, "clients"))
+ospath.insert(0, os.path.join(top_builddir, "tests"))
 os.environ["PATH"] = os.pathsep.join(ospath)
 
 # Parse the makefile in the current directory to identify the source directory
@@ -149,13 +151,38 @@ def stdtracks():
     maketrack("misc/blahblahblah.ogg")
     maketrack("Various/Greatest Hits/01:Jim Whatever - Spong.ogg")
     maketrack("Various/Greatest Hits/02:Joe Bloggs - Yadda.ogg")
+
+def bindable(p):
+    """bindable(P)
+
+    Return True iff UDP port P is bindable, else False"""
+    s = socket.socket(socket.AF_INET,
+                      socket.SOCK_DGRAM,
+                      socket.IPPROTO_UDP)
+    try:
+        s.bind(("127.0.0.1", p))
+        s.close()
+        return True
+    except:
+        return False
+
 def common_setup():
     remove_dir(testroot)
     os.mkdir(testroot)
+    # Choose a port
+    global port
+    port = random.randint(49152, 65535)
+    while not bindable(port + 1):
+        print "port %d is not bindable, trying another" % (port + 1)
+        port = random.randint(49152, 65535)
+    # Log anything sent to that port
+    packetlog = "%s/packetlog" % testroot
+    subprocess.Popen(["disorder-udplog",
+                      "--output", packetlog,
+                      "127.0.0.1", "%d" % port])
+    # disorder-udplog will quit when its parent process terminates
     open("%s/config" % testroot, "w").write(
-    """player *.ogg shell 'echo "$TRACK" >> %s/played.log'
-home %s
+    """home %s
 collection fs UTF-8 %s/tracks
 scratch %s/scratch.ogg
 gap 0
@@ -167,6 +194,7 @@ stopword the a an and to too in on of we i am as im for is
 username fred
 password fredpass
 allow fred fredpass
+trust fred
 plugins
 plugins %s/plugins
 plugins %s/plugins/.libs
@@ -178,7 +206,11 @@ tracklength *.mp3 disorder-tracklength
 tracklength *.ogg disorder-tracklength
 tracklength *.wav disorder-tracklength
 tracklength *.flac disorder-tracklength
-""" % (testroot, testroot, testroot, testroot, top_builddir, top_builddir))
+speaker_backend network
+broadcast 127.0.0.1 %d
+broadcast_from 127.0.0.1 %d
+""" % (testroot, testroot, testroot, top_builddir, top_builddir,
+       port, port + 1))
     copyfile("%s/sounds/scratch.ogg" % top_srcdir,
              "%s/scratch.ogg" % testroot)
 
@@ -186,8 +218,13 @@ def start_daemon():
     """start_daemon()
 
 Start the daemon."""
-    global daemon, errs
-    assert daemon == None
+    global daemon, errs, port
+    assert daemon == None, "no daemon running"
+    if not bindable(port + 1):
+        print "waiting for port %d to become bindable again..." % (port + 1)
+        time.sleep(1)
+        while not bindable(port + 1):
+            time.sleep(1)
     print " starting daemon"
     # remove the socket if it exists
     socket = "%s/socket" % testroot
@@ -226,7 +263,7 @@ Stop the daemon if it has not stopped already"""
     rc = daemon.poll()
     if rc == None:
         print " stopping daemon"
-        os.kill(daemon.pid, 15)
+        disorder.client().shutdown()
         print "  waiting for daemon"
         rc = daemon.wait()
         print "  daemon has stopped"
@@ -268,7 +305,7 @@ def run(module=None, report=True):
         except AssertionError, e:
             global failures
             failures += 1
-            print e
+            print "assertion failed: %s" % e.message
     finally:
         stop_daemon()
     if report:
@@ -290,15 +327,25 @@ Recursively delete directory D"""
         else:
             os.remove(d)
 
+def lists_have_same_contents(l1, l2):
+    """lists_have_same_contents(L1, L2)
+
+    Return True if L1 and L2 have equal members, in any order; else False."""
+    s1 = []
+    s1.extend(l1)
+    s1.sort()
+    s2 = []
+    s2.extend(l2)
+    s2.sort()
+    return s1 == s2
+
 def check_files():
     c = disorder.client()
     failures = 0
     for d in dirs_by_dir:
         xdirs = dirs_by_dir[d]
         dirs = c.directories(d)
-        xdirs.sort()
-        dirs.sort()
-        if dirs != xdirs:
+        if not lists_have_same_contents(xdirs, dirs):
             print
             print "directory: %s" % d
             print "expected:  %s" % xdirs
@@ -307,9 +354,7 @@ def check_files():
     for d in files_by_dir:
         xfiles = files_by_dir[d]
         files = c.files(d)
-        xfiles.sort()
-        files.sort()
-        if files != xfiles:
+        if not lists_have_same_contents(xfiles, files):
             print
             print "directory: %s" % d
             print "expected:  %s" % xfiles
@@ -317,6 +362,14 @@ def check_files():
             failures += 1
     return failures
 
+def command(args):
+    """Execute a command given as a list and return its stdout"""
+    p = subprocess.Popen(args, stdout=subprocess.PIPE)
+    lines = p.stdout.readlines()
+    rc = p.wait()
+    assert rc == 0, ("%s returned status %s" % (args, rc))
+    return lines
+
 # -----------------------------------------------------------------------------
 # Common setup
 
diff --git a/tests/dump.py b/tests/dump.py
new file mode 100755 (executable)
index 0000000..ff28c63
--- /dev/null
@@ -0,0 +1,88 @@
+#! /usr/bin/env python
+#
+# This file is part of DisOrder.
+# Copyright (C) 2007 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+import dtest,time,disorder,re
+
+def test():
+    """Exercise database dumper"""
+    dtest.start_daemon()
+    c = disorder.client()
+    track = "%s/Joe Bloggs/First Album/02:Second track.ogg" % dtest.tracks
+    dump = "%s/dumpfile" % dtest.testroot
+    print "setting a track pref"
+    c.set(track, "foo", "before")
+    assert c.get(track, "foo") == "before", "checking track foo=before"
+    print "setting a global pref"
+    c.setglobal("foo", "before");
+    assert c.getglobal("foo") == "before", "checking global foo=before"
+    print "adding a tag"
+    # Exercise the tags-changed code
+    c.set(track, "tags", "  first   tag, Another Tag")
+    assert dtest.lists_have_same_contents(c.tags(),
+                                          [u"another tag", u"first tag"]),\
+           "checking tag list(1)"
+    c.set(track, "tags", "wibble,   another tag   ")
+    assert dtest.lists_have_same_contents(c.tags(),
+                                          [u"another tag", u"wibble"]),\
+           "checking tag list(2)"
+    print "checking track appears in tag search"
+    tracks = c.search(["tag:wibble"])
+    assert len(tracks) == 1, "checking there is exactly one search result(1)"
+    assert tracks[0] == track, "checking for right search result(1)"
+    tracks = c.search(["tag:  another    tAg  "])
+    assert len(tracks) == 1, "checking there is exactly one search result(2)"
+    assert tracks[0] == track, "checking for right search result(2)"
+    print "dumping database"
+    print dtest.command(["disorder-dump", "--config", disorder._configfile,
+                         "--dump", dump])
+    print "changing track pref"
+    c.set(track, "foo", "after");
+    assert c.get(track, "foo") == "after", "checking track foo=before"
+    print "changing global pref"
+    c.setglobal("foo", "after");
+    assert c.getglobal("foo") == "after", "checking global foo=before"
+    print "adding fresh track pref"
+    c.set(track, "bar", "after")
+    print "adding fresh global pref"
+    c.setglobal("bar", "after")
+    dtest.stop_daemon();
+    print "restoring database"
+    print dtest.command(["disorder-dump", "--config", disorder._configfile,
+                         "--undump", dump])
+    dtest.start_daemon(); 
+    c = disorder.client()
+    print "checking track pref"
+    assert c.get(track, "foo") == "before", "checking track foo=before after undump"
+    print "checking global pref"
+    assert c.getglobal("foo") == "before", "checking global foo=before after undump"
+    print "checking fresh track pref"
+    assert c.get(track, "bar") is None, "checking fresh track pref has gone"
+    print "checking fresh global pref"
+    assert c.getglobal("bar") is None, "checking fresh global pref has gone"
+    print "checking tag search still works"
+    tracks = c.search(["tag:wibble"])
+    assert len(tracks) == 1, "checking there is exactly one search result"
+    assert tracks[0] == track, "checking for right search result(3)"
+    assert dtest.lists_have_same_contents(c.tags(),
+                                          [u"another tag", u"wibble"]),\
+           "checking tag list(3)"
+
+if __name__ == '__main__':
+    dtest.run()
index 3fc19f4577d3c427bbeac17e98b10ccc4737d2fe..69e98d4f9a6df494f49c2f05f5bed72978dd343d 100755 (executable)
@@ -23,7 +23,25 @@ import dtest,time,disorder,sys
 def test():
     """Check that the file listing comes out right"""
     dtest.start_daemon()
-    assert dtest.check_files() == 0
+    assert dtest.check_files() == 0, "dtest.check_files"
+    print " checking regexp file listing"
+    c = disorder.client()
+    f = c.files("%s/Joe Bloggs/First Album" % dtest.tracks,
+                "second")
+    assert len(f) == 1, "checking for one match"
+    assert f[0] == "%s/Joe Bloggs/First Album/02:Second track.ogg" % dtest.tracks
+    print " checking unicode regexp file listing"
+    f = c.files("%s/Joe Bloggs/First Album" % dtest.tracks,
+                "first")
+    assert len(f) == 0, "checking for 0 matches"
+    # This is rather unsatisfactory but it is the current behavior.  We could
+    # for instance go to NFD for regexp matching but we'd have to do the same
+    # to the regexp, including replacing single characters with (possibly
+    # bracketed) decomposed forms.  Really the answer has to be a more
+    # Unicode-aware regexp library.
+    f = c.files("%s/Joe Bloggs/First Album" % dtest.tracks,
+                "fi\\p{Mn}*rst")
+    assert len(f) == 0, "checking for 0 matches"
 
 if __name__ == '__main__':
     dtest.run()
diff --git a/tests/nothing.py b/tests/nothing.py
deleted file mode 100755 (executable)
index c12443e..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-#! /usr/bin/env python
-#
-# This file is part of DisOrder.
-# Copyright (C) 2007 Richard Kettlewell
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
-# USA
-#
-import dtest,time
-
-def test():
-    """Just start the server and then stop it"""
-    dtest.start_daemon()
-
-if __name__ == '__main__':
-    dtest.run()
diff --git a/tests/play.py b/tests/play.py
new file mode 100755 (executable)
index 0000000..92bb664
--- /dev/null
@@ -0,0 +1,75 @@
+#! /usr/bin/env python
+#
+# This file is part of DisOrder.
+# Copyright (C) 2007 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+import dtest,time,disorder,re
+
+def test():
+    """Play some tracks"""
+    dtest.start_daemon()
+    c = disorder.client()
+    track = u"%s/Joe Bloggs/First Album/02:Second track.ogg" % dtest.tracks
+    print "adding track to queue"
+    c.play(track)
+    print " checking track turned up in queue"
+    q = c.queue()
+    ts = filter(lambda t: t['track'] == track and 'submitter' in t, q)
+    assert len(ts) == 1, "checking track appears exactly once in queue"
+    t = ts[0]
+    assert t['submitter'] == u'fred', "check queue submitter"
+    i = t['id']
+    print " waiting for track"
+    p = c.playing()
+    r = c.recent()
+    while not((p is not None and p['id'] == i)
+              or (len(filter(lambda t: t['track'] == track and 'submitter' in t, r)) > 0)):
+        time.sleep(1)
+        p = c.playing()
+        r = c.recent()
+    print " checking track turned up in recent list"
+    while (p is not None and p['id'] == i):
+        time.sleep(1)
+        p = c.playing()
+    r = c.recent()
+    ts = filter(lambda t: t['track'] == track and 'submitter' in t, r)
+    assert len(ts) == 1, "check track appears exactly once in recent"
+    t = ts[0]
+    assert t['submitter'] == u'fred', "check recent entry submitter"
+    print " disabling play"
+    c.disable()
+    print " scratching current track"
+    p = c.playing()
+    i = p['id']
+    c.scratch(i)
+    print " checking scratched track turned up in recent list"
+    while (p is not None and p['id'] == i):
+        time.sleep(1)
+        p = c.playing()
+    r = c.recent()
+    ts = filter(lambda t: t['id'] == i, r)
+    assert len(ts) == 1, "check scratched track appears exactly once in recent"
+    assert ts[0]['state'] == 'scratched', "checking track scratched"
+    print " waiting for scratch to complete"
+    while (p is not None and p['state'] == 'isscratch'):
+        time.sleep(1)
+        p = c.playing()
+    assert p is None, "checking nothing is playing"
+
+if __name__ == '__main__':
+    dtest.run()
diff --git a/tests/queue.py b/tests/queue.py
new file mode 100755 (executable)
index 0000000..cc0b619
--- /dev/null
@@ -0,0 +1,78 @@
+#! /usr/bin/env python
+#
+# This file is part of DisOrder.
+# Copyright (C) 2007 Richard Kettlewell
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+#
+import dtest,time,disorder,re
+
+def test():
+    """Check the queue is padded to the (default) configured length"""
+    dtest.start_daemon()
+    c = disorder.client()
+    print " getting queue via python module"
+    q = c.queue()
+    assert len(q) == 10, "queue is at proper length"
+    print " getting queue via disorder(1)"
+    q = dtest.command(["disorder",
+                       "--config", disorder._configfile, "--no-per-user-config",
+                       "queue"])
+    tracks = filter(lambda s: re.match("^track", s), q)
+    assert len(tracks) == 10, "queue is at proper length"
+    print " disabling random play"
+    c.random_disable()
+    print " emptying queue"
+    for t in c.queue():
+        c.remove(t['id'])
+    print " checking queue is now empty"
+    q = c.queue()
+    assert q == [], "checking queue is empty"
+    print " enabling random play"
+    c.random_enable()
+    print " checking queue refills"
+    q = c.queue()
+    assert len(q) == 10, "queue is at proper length"
+    print " disabling all play"
+    c.random_disable()
+    c.disable()
+    print " emptying queue"
+    for t in c.queue():
+        c.remove(t['id'])
+    t1 = "%s/Joe Bloggs/Third Album/01:First_track.ogg" % dtest.tracks
+    t2 = "%s/Joe Bloggs/Third Album/02:Second_track.ogg" % dtest.tracks
+    t3 = "%s/Joe Bloggs/Third Album/02:Second_track.ogg" % dtest.tracks
+    print " adding tracks"
+    i1 = c.play(t1)
+    i2 = c.play(t2)
+    i3 = c.play(t3)
+    q = c.queue()
+    assert map(lambda e:e['id'], q) == [i1, i2, i3], "checking queue order(1)"
+    print " moving last track to start"
+    c.moveafter(None, [i3])
+    q = c.queue()
+    assert map(lambda e:e['id'], q) == [i3, i1, i2], "checking queue order(2)"
+    print " moving two tracks"
+    c.moveafter(i1, [i2, i3])
+    q = c.queue()
+    assert map(lambda e:e['id'], q) == [i1, i2 ,i3], "checking queue order(3)"
+    print " removing a track"
+    c.remove(i2)
+    q = c.queue()
+    assert map(lambda e:e['id'], q) == [i1 ,i3], "checking queue order(4)"
+
+if __name__ == '__main__':
+    dtest.run()
diff --git a/tests/udplog.c b/tests/udplog.c
new file mode 100644 (file)
index 0000000..eb87651
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2007 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <getopt.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <errno.h>
+#include <locale.h>
+#include <netdb.h>
+#include <unistd.h>
+#include <ctype.h>
+
+#include "configuration.h"
+#include "syscalls.h"
+#include "log.h"
+#include "addr.h"
+#include "defs.h"
+#include "mem.h"
+
+static const struct option options[] = {
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+  { "output", required_argument, 0, 'o' },
+  { 0, 0, 0, 0 }
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+         "  disorder-udplog [OPTIONS] ADDRESS PORT\n"
+         "Options:\n"
+         "  --output, -o PATH       Output to PATH (default: stdout)\n"
+         "  --help, -h              Display usage message\n"
+         "  --version, -V           Display version number\n"
+          "\n"
+          "UDP packet receiver.\n");
+  xfclose(stdout);
+  exit(0);
+}
+
+/* display version number and terminate */
+static void version(void) {
+  xprintf("%s", disorder_version_string);
+  xfclose(stdout);
+  exit(0);
+}
+
+int main(int argc, char **argv) {
+  int n, fd, err, i, j;
+  struct addrinfo *ai;
+  struct stringlist a;
+  char *name, h[4096], s[4096];
+  uint8_t buffer[4096];
+  union {
+    struct sockaddr sa;
+    struct sockaddr_in sin;
+    struct sockaddr_in6 sin6;
+  } sa;
+  socklen_t len;
+  static const struct addrinfo pref = {
+    0,                         /* ai_flags */
+    AF_UNSPEC,                 /* ai_family */
+    SOCK_DGRAM,                        /* ai_socktype */
+    IPPROTO_UDP,               /* ai_protocol */
+    0,
+    0,
+    0,
+    0
+  };
+  
+  set_progname(argv);
+  mem_init();
+  if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
+  while((n = getopt_long(argc, argv, "hVo:", options, 0)) >= 0) {
+    switch(n) {
+    case 'h': help();
+    case 'V': version();
+    case 'o':
+      if(!freopen(optarg, "w", stdout))
+       fatal(errno, "%s", optarg);
+      break;
+    default: fatal(0, "invalid option");
+    }
+  }
+  if(optind + 2 != argc)
+    fatal(0, "missing arguments");
+  a.n = 2;
+  a.s = &argv[optind];
+  if(!(ai = get_address(&a, &pref, &name)))
+    exit(1);
+  fd = xsocket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+  if(bind(fd, ai->ai_addr, ai->ai_addrlen) < 0)
+    fatal(errno, "error binding to %s", name);
+  while(getppid() != 1) {
+    len = sizeof sa;
+    n = recvfrom(fd, buffer, sizeof buffer, 0, &sa.sa, &len);
+    if(n < 0) {
+      if(errno == EINTR || errno == EAGAIN)
+       continue;
+      fatal(errno, "%s: recvfrom", name);
+    }
+    if((err = getnameinfo(&sa.sa, len, h, sizeof h, s, sizeof s,
+                         NI_NUMERICHOST|NI_NUMERICSERV|NI_DGRAM)))
+      fatal(0, "getnameinfo: %s", gai_strerror(err));
+    xprintf("from host %s service %s: %d bytes\n", h, s, n);
+    for(i = 0; i < n; i += 16) {
+      for(j = i; j < n && j < i + 16; ++j)
+       xprintf(" %02x", buffer[j]);
+      for(; j < i + 16; ++j)
+       xprintf("   ");
+      xprintf("  ");
+      for(j = i; j < n && j < i + 16; ++j)
+       xprintf("%c", buffer[j] < 128 && isprint(buffer[j]) ? buffer[j] : '.');
+      xprintf("\n");
+      if(fflush(stdout) < 0)
+       fatal(errno, "stdout");
+    }
+  }
+  return 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/tests/version.py b/tests/version.py
deleted file mode 100755 (executable)
index 9ec922c..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-#! /usr/bin/env python
-#
-# This file is part of DisOrder.
-# Copyright (C) 2007 Richard Kettlewell
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
-# USA
-#
-import dtest,time,disorder
-
-def test():
-    """Ask the server its version number"""
-    dtest.start_daemon()
-    c = disorder.client()
-    v = c.version()
-    print "Server version: %s" % v
-
-if __name__ == '__main__':
-    dtest.run()