From: Richard Kettlewell Date: Sun, 1 Mar 2009 17:47:14 +0000 (+0000) Subject: Merge latest work from uniform audio branch. The only functional change X-Git-Tag: 5.0~183 X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/commitdiff_plain/94ddd3e3779fee737969ffcbb0e8f161548d4e0f?hp=4f8132b3d83dc46d4ab2321ef5b838da406bd100 Merge latest work from uniform audio branch. The only functional change here is that disorder-playrtp supports playing into a command via a pipe now. --- diff --git a/clients/playrtp.c b/clients/playrtp.c index f5103a3..03e4ad2 100644 --- a/clients/playrtp.c +++ b/clients/playrtp.c @@ -216,6 +216,7 @@ static const struct option options[] = { { "core-audio", no_argument, 0, 'c' }, #endif { "dump", required_argument, 0, 'r' }, + { "command", required_argument, 0, 'e' }, { "socket", required_argument, 0, 's' }, { "config", required_argument, 0, 'C' }, { 0, 0, 0, 0 } @@ -491,6 +492,7 @@ static void help(void) { #if HAVE_COREAUDIO_AUDIOHARDWARE_H " --core-audio, -c Use Core Audio to play audio\n" #endif + " --command, -e COMMAND Pipe audio to command\n" " --help, -h Display usage message\n" " --version, -V Display version number\n" ); @@ -577,7 +579,6 @@ int main(int argc, char **argv) { }; union any_sockaddr mgroup; const char *dumpfile = 0; - const char *device = 0; pthread_t ltid; static const struct addrinfo prefs = { @@ -590,12 +591,12 @@ int main(int argc, char **argv) { mem_init(); if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale"); backend = uaudio_apis[0]; - while((n = getopt_long(argc, argv, "hVdD:m:b:x:L:R:M:aocC:r", options, 0)) >= 0) { + while((n = getopt_long(argc, argv, "hVdD:m:b:x:L:R:M:aocC:re:", options, 0)) >= 0) { switch(n) { case 'h': help(); case 'V': version("disorder-playrtp"); case 'd': debugging = 1; break; - case 'D': device = optarg; break; + case 'D': uaudio_set("device", optarg); break; case 'm': minbuffer = 2 * atol(optarg); break; case 'b': readahead = 2 * atol(optarg); break; case 'x': maxbuffer = 2 * atol(optarg); break; @@ -613,6 +614,7 @@ int main(int argc, char **argv) { case 'C': configfile = optarg; break; case 's': control_socket = optarg; break; case 'r': dumpfile = optarg; break; + case 'e': backend = &uaudio_command; uaudio_set("command", optarg); break; default: fatal(0, "invalid option"); } } @@ -742,9 +744,6 @@ int main(int argc, char **argv) { fatal(errno, "mapping %s", dumpfile); info("dumping to %s", dumpfile); } - /* Choose output device */ - if(device) - uaudio_set("device", device); /* Set up output. Currently we only support L16 so there's no harm setting * the format before we know what it is! */ uaudio_set_format(44100/*Hz*/, 2/*channels*/, diff --git a/doc/disorder-playrtp.1.in b/doc/disorder-playrtp.1.in index 440604f..ea4c93f 100644 --- a/doc/disorder-playrtp.1.in +++ b/doc/disorder-playrtp.1.in @@ -40,14 +40,17 @@ multicast to that group address and port. 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 +.B \-\-alsa\fR, \fB\-a Use ALSA to play sound. +Only available on Linux. .TP .B \-\-oss\fR, \fB\-o Use OSS to play sound. +Only available on Linux and FreeBSD. .TP .B \-\-core\-audio\fR, \fB\-c Use Core Audio to play sound. +Only available on Macs. .TP .B \-\-device \fIDEVICE\fR, \fB\-D \fIDEVICE\fR Specifies the audio device to use. @@ -55,6 +58,23 @@ See .B "DEVICE NAMES" below for more information. .TP +.B \-\-command \fICOMMAND\fR, \fB-e \fICOMMAND\fR +Instead of sending to a physical audio device, invoke \fICOMMAND\fR using the +shell and write audio samples to its standard input. +Currently the input will be 44100KHz 16-bit signed stereo samples. +If \fICOMMAND\fR exits it is re-executed; any samples that had been written to +the pipe but not processed by the previous instance will be lost. +.IP +.B \-\-device +is redundant with this option. +.IP +As an example, +.B "-e \(aqcat > dump\(aq" +would log audio data to a file for later processing. +You could convert it to another format with, for instance: +.IP +.B "sox -c2 -traw -r44100 -s -w dump dump.wav" +.TP .B \-\-config \fIPATH\fR, \fB\-C \fIPATH Set the configuration file. The default is diff --git a/lib/Makefile.am b/lib/Makefile.am index fe6a661..6a6596f 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -85,7 +85,7 @@ libdisorder_a_SOURCES=charset.c charset.h \ uaudio.c uaudio-thread.c uaudio.h \ uaudio-oss.c uaudio-alsa.c \ uaudio-coreaudio.c \ - uaudio-rtp.c \ + uaudio-rtp.c uaudio-command.c \ url.h url.c \ user.h user.c \ unicode.h unicode.c \ diff --git a/lib/uaudio-command.c b/lib/uaudio-command.c new file mode 100644 index 0000000..6b7ef66 --- /dev/null +++ b/lib/uaudio-command.c @@ -0,0 +1,145 @@ +/* + * This file is part of DisOrder. + * Copyright (C) 2005, 2006, 2007, 2009 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 + * the Free Software Foundation, either version 3 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, see . + */ +/** @file lib/uaudio-command.c + * @brief Support for commmand backend */ +#include "common.h" + +#include +#include + +#include "syscalls.h" +#include "log.h" +#include "mem.h" +#include "wstat.h" +#include "uaudio.h" + +/** @brief Pipe to subprocess */ +static int command_fd; + +/** @brief Child process ID */ +static pid_t command_pid; + +static const char *const command_options[] = { + "command", + NULL +}; + +/** @brief Close pipe and wait for subprocess to terminate */ +static void command_wait(void) { + int w; + pid_t rc; + char *ws; + + close(command_fd); + while((rc = waitpid(command_pid, &w, 0) < 0 && errno == EINTR)) + ; + if(rc < 0) + fatal(errno, "waitpid"); + if(w) { + ws = wstat(w); + error(0, "command subprocess %s", ws); + xfree(ws); + } +} + +/** @brief Create subprocess */ +static void command_open(void) { + int pfd[2]; + const char *command; + + if(!(command = uaudio_get("command"))) + fatal(0, "'command' not set"); + xpipe(pfd); + command_pid = xfork(); + if(!command_pid) { + exitfn = _exit; + signal(SIGPIPE, SIG_DFL); + xdup2(pfd[0], 0); + close(pfd[0]); + close(pfd[1]); + /* TODO it would be nice to set some environment variables given the sample + * format. The original intended model is that you adapt DisOrder to the + * command you run but it'd be nice to support the opposite. */ + execl("/bin/sh", "sh", "-c", command, (char *)0); + fatal(errno, "error executing /bin/sh"); + } + close(pfd[0]); + command_fd = pfd[1]; +} + +/** @brief Send audio data to subprocess */ +static size_t command_play(void *buffer, size_t nsamples) { + const size_t bytes = nsamples * uaudio_sample_size; + int written = write(command_fd, buffer, bytes); + if(written < 0) { + switch(errno) { + case EINTR: + return 0; /* will retry */ + case EPIPE: + error(0, "audio command subprocess terminated"); + command_wait(); + command_open(); + return 0; /* will retry */ + default: + fatal(errno, "error writing to audio command subprocess"); + } + } + return written / uaudio_sample_size; +} + +static void command_start(uaudio_callback *callback, + void *userdata) { + command_open(); + uaudio_thread_start(callback, + userdata, + command_play, + uaudio_channels, + 4096 / uaudio_sample_size); +} + +static void command_stop(void) { + uaudio_thread_stop(); + command_wait(); +} + +static void command_activate(void) { + uaudio_thread_activate(); +} + +static void command_deactivate(void) { + uaudio_thread_deactivate(); +} + +const struct uaudio uaudio_command = { + .name = "command", + .options = command_options, + .start = command_start, + .stop = command_stop, + .activate = command_activate, + .deactivate = command_deactivate +}; + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/uaudio-rtp.c b/lib/uaudio-rtp.c index 1bbfa9c..4785d62 100644 --- a/lib/uaudio-rtp.c +++ b/lib/uaudio-rtp.c @@ -15,38 +15,375 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -/** @file lib/uaudio-oss.c +/** @file lib/uaudio-rtp.c * @brief Support for RTP network play backend */ #include "common.h" -#include +#include +#include +#include +#include +#include +#include #include "uaudio.h" #include "mem.h" #include "log.h" #include "syscalls.h" +#include "rtp.h" +#include "addr.h" +#include "ifreq.h" +#include "timeval.h" + +/** @brief Bytes to send per network packet + * + * This is the maximum number of bytes we pass to write(2); to determine actual + * packet sizes, add a UDP header and an IP header (and a link layer header if + * it's the link layer size you care about). + * + * Don't make this too big or arithmetic will start to overflow. + */ +#define NETWORK_BYTES (1500-8/*UDP*/-40/*IP*/-8/*conservatism*/) + +/** @brief RTP payload type */ +static int rtp_payload; + +/** @brief RTP output socket */ +static int rtp_fd; + +/** @brief RTP SSRC */ +static uint32_t rtp_id; + +/** @brief RTP sequence number */ +static uint16_t rtp_sequence; + +/** @brief RTP timestamp + * + * This is the timestamp that will be used on the next outbound packet. + * + * The timestamp in the packet header is only 32 bits wide. With 44100Hz + * stereo, that only gives about half a day before wrapping, which is not + * particularly convenient for certain debugging purposes. Therefore the + * timestamp is maintained as a 64-bit integer, giving around six million years + * before wrapping, and truncated to 32 bits when transmitting. + */ +static uint64_t rtp_timestamp; + +/** @brief Actual time corresponding to @ref rtp_timestamp + * + * This is the time, on this machine, at which the sample at @ref rtp_timestamp + * ought to be sent, interpreted as the time the last packet was sent plus the + * time length of the packet. */ +static struct timeval rtp_timeval; + +/** @brief Set when we (re-)activate, to provoke timestamp resync */ +static int rtp_reactivated; + +/** @brief Network error count + * + * If too many errors occur in too short a time, we give up. + */ +static int rtp_errors; + +/** @brief Delay threshold in microseconds + * + * rtp_play() never attempts to introduce a delay shorter than this. + */ +static int64_t rtp_delay_threshold; static const char *const rtp_options[] = { + "rtp-destination", + "rtp-destination-port", + "rtp-source", + "rtp-source-port", + "multicast-ttl", + "multicast-loop", + "rtp-delay-threshold", NULL }; +static size_t rtp_play(void *buffer, size_t nsamples) { + struct rtp_header header; + struct iovec vec[2]; + struct timeval now; + + /* We do as much work as possible before checking what time it is */ + /* Fill out header */ + header.vpxcc = 2 << 6; /* V=2, P=0, X=0, CC=0 */ + header.seq = htons(rtp_sequence++); + header.ssrc = rtp_id; + header.mpt = (rtp_reactivated ? 0x80 : 0x00) | rtp_payload; +#if !WORDS_BIGENDIAN + /* Convert samples to network byte order */ + uint16_t *u = buffer, *const limit = u + nsamples; + while(u < limit) { + *u = htons(*u); + ++u; + } +#endif + vec[0].iov_base = (void *)&header; + vec[0].iov_len = sizeof header; + vec[1].iov_base = buffer; + vec[1].iov_len = nsamples * uaudio_sample_size; +retry: + xgettimeofday(&now, NULL); + if(rtp_reactivated) { + /* We've been deactivated for some unknown interval. We need to advance + * rtp_timestamp to account for the dead air. */ + /* On the first run through we'll set the start time. */ + if(!rtp_timeval.tv_sec) + rtp_timeval = now; + /* See how much time we missed. + * + * This will be 0 on the first run through, in which case we'll not modify + * anything. + * + * It'll be negative in the (rare) situation where the deactivation + * interval is shorter than the last packet we sent. In this case we wait + * for that much time and then return having sent no samples, which will + * cause uaudio_play_thread_fn() to retry. + * + * In the normal case it will be positive. + */ + const int64_t delay = tvsub_us(now, rtp_timeval); /* microseconds */ + if(delay < 0) { + usleep(-delay); + goto retry; + } + /* Advance the RTP timestamp to the present. With 44.1KHz stereo this will + * overflow the intermediate value with a delay of a bit over 6 years. + * This seems acceptable. */ + uint64_t update = (delay * uaudio_rate * uaudio_channels) / 1000000; + /* Don't throw off channel synchronization */ + update -= update % uaudio_channels; + /* We log nontrivial changes */ + if(update) + info("advancing rtp_time by %"PRIu64" samples", update); + rtp_timestamp += update; + rtp_timeval = now; + rtp_reactivated = 0; + } else { + /* Chances are we've been called right on the heels of the previous packet. + * If we just sent packets as fast as we got audio data we'd get way ahead + * of the player and some buffer somewhere would fill (or at least become + * unreasonably large). + * + * First find out how far ahead of the target time we are. + */ + const int64_t ahead = tvsub_us(now, rtp_timeval); /* microseconds */ + /* Only delay at all if we are nontrivially ahead. */ + if(ahead > rtp_delay_threshold) { + /* Don't delay by the full amount */ + usleep(ahead - rtp_delay_threshold / 2); + /* Refetch time (so we don't get out of step with reality) */ + xgettimeofday(&now, NULL); + } + } + header.timestamp = htonl((uint32_t)rtp_timestamp); + int written_bytes; + do { + written_bytes = writev(rtp_fd, vec, 2); + } while(written_bytes < 0 && errno == EINTR); + if(written_bytes < 0) { + error(errno, "error transmitting audio data"); + ++rtp_errors; + if(rtp_errors == 10) + fatal(0, "too many audio tranmission errors"); + return 0; + } else + rtp_errors /= 2; /* gradual decay */ + written_bytes -= sizeof (struct rtp_header); + size_t written_samples = written_bytes / uaudio_sample_size; + /* rtp_timestamp and rtp_timestamp are supposed to refer to the first sample + * of the next packet */ + rtp_timestamp += written_samples; + const unsigned usec = (rtp_timeval.tv_usec + + 1000000 * written_samples / (uaudio_rate + * uaudio_channels)); + /* ...will only overflow 32 bits if one packet is more than about half an + * hour long, which is not plausible. */ + rtp_timeval.tv_sec += usec / 1000000; + rtp_timeval.tv_usec = usec % 1000000; + return written_samples; +} + +static void rtp_open(void) { + struct addrinfo *res, *sres; + static const struct addrinfo pref = { + .ai_flags = 0, + .ai_family = PF_INET, + .ai_socktype = SOCK_DGRAM, + .ai_protocol = IPPROTO_UDP, + }; + static const struct addrinfo prefbind = { + .ai_flags = AI_PASSIVE, + .ai_family = PF_INET, + .ai_socktype = SOCK_DGRAM, + .ai_protocol = IPPROTO_UDP, + }; + static const int one = 1; + int sndbuf, target_sndbuf = 131072; + socklen_t len; + char *sockname, *ssockname; + struct stringlist dst, src; + const char *delay; + + /* Get configuration */ + dst.n = 2; + dst.s = xcalloc(2, sizeof *dst.s); + dst.s[0] = uaudio_get("rtp-destination"); + dst.s[1] = uaudio_get("rtp-destination-port"); + src.n = 2; + src.s = xcalloc(2, sizeof *dst.s); + src.s[0] = uaudio_get("rtp-source"); + src.s[1] = uaudio_get("rtp-source-port"); + if(!dst.s[0]) + fatal(0, "'rtp-destination' not set"); + if(!dst.s[1]) + fatal(0, "'rtp-destination-port' not set"); + if(src.s[0]) { + if(!src.s[1]) + fatal(0, "'rtp-source-port' not set"); + src.n = 2; + } else + src.n = 0; + if((delay = uaudio_get("rtp-delay-threshold"))) + rtp_delay_threshold = atoi(delay); + else + rtp_delay_threshold = 1000; /* microseconds */ + + /* Resolve addresses */ + res = get_address(&dst, &pref, &sockname); + if(!res) exit(-1); + if(src.n) { + sres = get_address(&src, &prefbind, &ssockname); + if(!sres) exit(-1); + } else + sres = 0; + /* Create the socket */ + if((rtp_fd = socket(res->ai_family, + res->ai_socktype, + res->ai_protocol)) < 0) + fatal(errno, "error creating broadcast socket"); + if(multicast(res->ai_addr)) { + /* Enable multicast options */ + const char *ttls = uaudio_get("multicast-ttl"); + const int ttl = ttls ? atoi(ttls) : 1; + const char *loops = uaudio_get("multicast-loop"); + const int loop = loops ? !strcmp(loops, "yes") : 1; + switch(res->ai_family) { + case PF_INET: { + if(setsockopt(rtp_fd, IPPROTO_IP, IP_MULTICAST_TTL, + &ttl, sizeof ttl) < 0) + fatal(errno, "error setting IP_MULTICAST_TTL on multicast socket"); + if(setsockopt(rtp_fd, IPPROTO_IP, IP_MULTICAST_LOOP, + &loop, sizeof loop) < 0) + fatal(errno, "error setting IP_MULTICAST_LOOP on multicast socket"); + break; + } + case PF_INET6: { + if(setsockopt(rtp_fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, + &ttl, sizeof ttl) < 0) + fatal(errno, "error setting IPV6_MULTICAST_HOPS on multicast socket"); + if(setsockopt(rtp_fd, IPPROTO_IP, IPV6_MULTICAST_LOOP, + &loop, sizeof loop) < 0) + fatal(errno, "error setting IPV6_MULTICAST_LOOP on multicast socket"); + break; + } + default: + fatal(0, "unsupported address family %d", res->ai_family); + } + info("multicasting on %s TTL=%d loop=%s", + sockname, ttl, loop ? "yes" : "no"); + } else { + struct ifaddrs *ifs; + + if(getifaddrs(&ifs) < 0) + fatal(errno, "error calling getifaddrs"); + while(ifs) { + /* (At least on Darwin) IFF_BROADCAST might be set but ifa_broadaddr + * still a null pointer. It turns out that there's a subsequent entry + * for he same interface which _does_ have ifa_broadaddr though... */ + if((ifs->ifa_flags & IFF_BROADCAST) + && ifs->ifa_broadaddr + && sockaddr_equal(ifs->ifa_broadaddr, res->ai_addr)) + break; + ifs = ifs->ifa_next; + } + if(ifs) { + if(setsockopt(rtp_fd, SOL_SOCKET, SO_BROADCAST, &one, sizeof one) < 0) + fatal(errno, "error setting SO_BROADCAST on broadcast socket"); + info("broadcasting on %s (%s)", sockname, ifs->ifa_name); + } else + info("unicasting on %s", sockname); + } + /* Enlarge the socket buffer */ + len = sizeof sndbuf; + if(getsockopt(rtp_fd, SOL_SOCKET, SO_SNDBUF, + &sndbuf, &len) < 0) + fatal(errno, "error getting SO_SNDBUF"); + if(target_sndbuf > sndbuf) { + if(setsockopt(rtp_fd, SOL_SOCKET, SO_SNDBUF, + &target_sndbuf, sizeof target_sndbuf) < 0) + error(errno, "error setting SO_SNDBUF to %d", target_sndbuf); + else + info("changed socket send buffer size from %d to %d", + sndbuf, target_sndbuf); + } else + info("default socket send buffer is %d", + sndbuf); + /* We might well want to set additional broadcast- or multicast-related + * options here */ + if(sres && bind(rtp_fd, sres->ai_addr, sres->ai_addrlen) < 0) + fatal(errno, "error binding broadcast socket to %s", ssockname); + if(connect(rtp_fd, res->ai_addr, res->ai_addrlen) < 0) + fatal(errno, "error connecting broadcast socket to %s", sockname); + /* Various fields are required to have random initial values by RFC3550. The + * packet contents are highly public so there's no point asking for very + * strong randomness. */ + gcry_create_nonce(&rtp_id, sizeof rtp_id); + gcry_create_nonce(&rtp_sequence, sizeof rtp_sequence); + gcry_create_nonce(&rtp_timestamp, sizeof rtp_timestamp); + /* rtp_play() will spot this and choose an initial value */ + rtp_timeval.tv_sec = 0; +} + static void rtp_start(uaudio_callback *callback, void *userdata) { - (void)callback; - (void)userdata; - /* TODO */ + /* We only support L16 (but we do stereo and mono and will convert sign) */ + if(uaudio_channels == 2 + && uaudio_bits == 16 + && uaudio_rate == 44100) + rtp_payload = 10; + else if(uaudio_channels == 1 + && uaudio_bits == 16 + && uaudio_rate == 44100) + rtp_payload = 11; + else + fatal(0, "asked for %d/%d/%d 16/44100/1 and 16/44100/2", + uaudio_bits, uaudio_rate, uaudio_channels); + rtp_open(); + uaudio_thread_start(callback, + userdata, + rtp_play, + 256 / uaudio_sample_size, + (NETWORK_BYTES - sizeof(struct rtp_header)) + / uaudio_sample_size); } static void rtp_stop(void) { - /* TODO */ + uaudio_thread_stop(); + close(rtp_fd); + rtp_fd = -1; } static void rtp_activate(void) { - /* TODO */ + rtp_reactivated = 1; + uaudio_thread_activate(); } static void rtp_deactivate(void) { - /* TODO */ + uaudio_thread_deactivate(); } const struct uaudio uaudio_rtp = { diff --git a/lib/uaudio.c b/lib/uaudio.c index f18af09..266846e 100644 --- a/lib/uaudio.c +++ b/lib/uaudio.c @@ -57,7 +57,7 @@ void uaudio_set(const char *name, const char *value) { } /** @brief Set a uaudio option */ -const char *uaudio_get(const char *name) { +char *uaudio_get(const char *name) { const char *value = (uaudio_options ? *(char **)hash_find(uaudio_options, name) : NULL); @@ -107,6 +107,7 @@ const struct uaudio *uaudio_apis[] = { &uaudio_oss, #endif &uaudio_rtp, + &uaudio_command, NULL, }; diff --git a/lib/uaudio.h b/lib/uaudio.h index ac88b3f..a6996f8 100644 --- a/lib/uaudio.h +++ b/lib/uaudio.h @@ -94,7 +94,7 @@ struct uaudio { void uaudio_set_format(int rate, int channels, int samplesize, int signed_); void uaudio_set(const char *name, const char *value); -const char *uaudio_get(const char *name); +char *uaudio_get(const char *name); void uaudio_thread_start(uaudio_callback *callback, void *userdata, uaudio_playcallback *playcallback, @@ -117,6 +117,7 @@ extern const struct uaudio uaudio_oss; #endif extern const struct uaudio uaudio_rtp; +extern const struct uaudio uaudio_command; extern const struct uaudio *uaudio_apis[]; diff --git a/lib/wstat.c b/lib/wstat.c index dec036e..0ac2c17 100644 --- a/lib/wstat.c +++ b/lib/wstat.c @@ -1,6 +1,6 @@ /* * This file is part of DisOrder. - * Copyright (C) 2004, 2007, 2008 Richard Kettlewell + * Copyright (C) 2004, 2007-2009 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 @@ -32,7 +32,7 @@ * @param w Exit status (e.g. from waitpid()) * @return Allocated string containing description of status */ -const char *wstat(int w) { +char *wstat(int w) { int n; char *r; @@ -48,7 +48,7 @@ const char *wstat(int w) { else n = byte_xasprintf(&r, "terminated with unknown wait status %#x", (unsigned)w); - return n >= 0 ? r : "[could not convert wait status]"; + return n >= 0 ? r : xstrdup("[could not convert wait status]"); } /* diff --git a/lib/wstat.h b/lib/wstat.h index aebe6ef..b6deb8a 100644 --- a/lib/wstat.h +++ b/lib/wstat.h @@ -1,6 +1,6 @@ /* * This file is part of DisOrder. - * Copyright (C) 2004, 2007, 2008 Richard Kettlewell + * Copyright (C) 2004, 2007-2009 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 @@ -24,7 +24,7 @@ #include -const char *wstat(int w); +char *wstat(int w); /* Format wait status @w@. In extremis the return value might be a * pointer to a string literal. The result should always be ASCII. */