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
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
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
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
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
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
* 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
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:
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
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).
<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
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
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
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:
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
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])
+])
bin_PROGRAMS=disorder disorderfm disorder-playrtp
noinst_PROGRAMS=test-eclient filename-bytes
+noinst_SCRIPTS=dump2wav
AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
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
diff -u ,commands ,completions
CLEANFILES=,commands ,completions
+
+EXTRA_DIST=dump2wav
{ "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 }
};
/* 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);
}
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();
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");
}
}
/* 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);
}
--- /dev/null
+#! /bin/bash
+#
+# Usage: dump2wav DUMPFILE [sox format options] OUTPUT
+#
+exec sox -r 44100 -s -w -c 2 "$@"
#include <alsa/asoundlib.h>
#include <assert.h>
#include <pthread.h>
+#include <arpa/inet.h>
#include "mem.h"
#include "log.h"
} 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;
}
}
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
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;
/*
* 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
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
+#include <arpa/inet.h>
#include "mem.h"
#include "log.h"
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;
}
#include <sys/time.h>
#include <sys/un.h>
#include <unistd.h>
+#include <sys/mman.h>
+#include <fcntl.h>
#include "log.h"
#include "mem.h"
/** @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' },
#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 }
/* 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);
}
struct sockaddr_in6 in6;
};
union any_sockaddr mgroup;
+ const char *dumpfile = 0;
static const struct addrinfo prefs = {
AI_PASSIVE,
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();
#endif
case 'C': configfile = optarg; break;
case 's': control_socket = optarg; break;
+ case 'r': dumpfile = optarg; break;
default: fatal(0, "invalid option");
}
}
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;
}
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 */
#
# 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
# 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])
if test $rjk_cv_shadow = yes; then
CC="${CC} -Wshadow"
fi
-
-
fi
+RJK_GCOV
+
AH_BOTTOM([#ifdef __GNUC__
# define attribute(x) __attribute__(x)
#else
+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
--- /dev/null
+/etc/bash_completion.d/disorder
$(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 \
$(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
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 \
/* 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);
}
/* 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);
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
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*/);
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*/);
.\" .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
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
.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
.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
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:
.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
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
.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
.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)
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
.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:
.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
.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)
.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
.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
.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
.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
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,
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
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
.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)
.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.
.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.
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.
.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).
.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.
.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
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
.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
.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
.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
\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.
.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
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.
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.
.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
.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
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
.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.
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).
.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
to
.IR DESTINATION ,
transforming filenames along the way.
+.PP
+This program is not well-tested!
.SH OPTIONS
.SS "Filename Format"
.TP
.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
.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:
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
@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
* 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"
/*
* 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
* 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
}
/** @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
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);
/*
* 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 */
{ 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 },
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;
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 */
/** @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 */
/* get the private config file */
extern char *configfile;
+extern int config_per_user;
#endif /* CONFIGURATION_H */
#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;
/** @brief Package documentation directory */
const char docdir[] = DOCDIR;
+#include "version.h"
+
/*
Local Variables:
c-basic-offset:2
#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[];
vector_init(&c->vec);
dynstr_init(&c->input);
dynstr_init(&c->output);
- if(!config->password) {
- error(0, "no password set");
- return 0;
- }
return c;
}
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
/*
* 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
* 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
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);
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);
/*
* 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
* 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
#include <string.h>
#include <ctype.h>
+#include <stdio.h>
+
#include "mem.h"
#include "mime.h"
#include "vector.h"
&& (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) {
&& !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
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,
* '-' 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;
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;
}
} 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;
} 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;
}
++ptr;
c->precision = -1;
} else
- return -1;
+ c->precision = 0;
c->flags |= f_precision;
}
/* length modifier */
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 */
/*
* 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
* 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);
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);
}
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);
}
/*
* 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
* 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
* 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;
/* 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) {
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);
/* 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);
* 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);
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);
}
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)) {
#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;
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;
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");
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"));
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");
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;
}
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);
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();
test_hex();
/* inputline.c */
/* kvp.c */
+ test_kvp();
/* log.c */
/* mem.c */
/* mime.c */
/* 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 */
/* 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;
}
+++ /dev/null
-/*
- * 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:
-*/
+++ /dev/null
-/*
- * 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:
-*/
* 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;
* 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
#
-# 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
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
while True:
k = i.next()
v = i.next()
- d[k] = v
+ d[str(k)] = v
except StopIteration:
pass
return d
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:
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.
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:
"""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):
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.
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.
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.
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
#
# 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':
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)
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
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
--- /dev/null
+#! /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")
--- /dev/null
+#! /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";
#! /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;
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
/* 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);
}
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,
/* 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);
}
/* 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);
}
{ "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 }
/* 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);
}
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();
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");
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. */
/* 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);
}
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);
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),
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) {
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':
/* 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);
}
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. */
return START_SOFTFAIL;
}
store_player_pid(q->id, pid);
+ q->prepared = 1;
if(lfd != -1)
xclose(lfd);
setpgid(pid, pid);
/* 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);
}
#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;
* 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);
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 */
}
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;
}
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;
}
/*
* 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
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: {
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:
* 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) {
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 */
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;
}
/*
* 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
/*
* 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
/* 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);
}
* 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;
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;
}
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.
* - 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) {
* 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;
}
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;
/* 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 */
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);
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:
paused = 0;
/* As for SM_PLAY we attempt to play straight away. */
if(playing)
- play(3 * FRAMES);
+ speaker_play(3 * FRAMES);
}
report();
break;
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
*/
#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
/** @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
trackdb_close();
trackdb_deinit();
info("terminating");
- _exit(0);
+ exit(0);
}
static void reset_socket(ev_source *ev) {
/* 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);
}
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 */
* - @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 */
/* 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",
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];
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) {
/* 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;
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;
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;
}
}
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) {
static void stats_complete(struct stats_details *d) {
char *s;
-
+
if(!(d->exited && d->closed))
return;
byte_xasprintf(&s, "\n"
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;
*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
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) {
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 */
void trackdb_close(void);
/* open/close track databases */
+extern int trackdb_existing_database;
+
char **trackdb_stats(int *nstatsp);
/* return a list of database stats */
/* 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);
}
# 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
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()
"""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"""
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
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
username fred
password fredpass
allow fred fredpass
+trust fred
plugins
plugins %s/plugins
plugins %s/plugins/.libs
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)
"""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
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"
except AssertionError, e:
global failures
failures += 1
- print e
+ print "assertion failed: %s" % e.message
finally:
stop_daemon()
if report:
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
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
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
--- /dev/null
+#! /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()
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()
+++ /dev/null
-#! /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()
--- /dev/null
+#! /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()
--- /dev/null
+#! /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()
--- /dev/null
+/*
+ * 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:
+*/
+++ /dev/null
-#! /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()