diff --git a/HISTORY b/HISTORY index 838cc65f..2e58d0da 100644 --- a/HISTORY +++ b/HISTORY @@ -56,7 +56,7 @@ Video Disk Recorder Revision History the PC keyboard to better resemble the "up-down-left-right-ok" layout on menu controlling remote control units. -2000-07-15: Version 0.06 +2000-07-23: Version 0.06 - Added support for LIRC remote control (thanks to Carsten Koch!). There are now three different remote control modes: KBD (PC-Keyboard), RCU @@ -83,3 +83,6 @@ Video Disk Recorder Revision History - The polarization can now be given in uppercase or lowercase characters in channels.conf. - Fixed buffer initialization to work with DVB driver version 0.6. +- Implemented the "Simple Video Disk Recorder Protocol" (SVDRP) to control + the VDR over a network connection. +- Implemented command line option handling. diff --git a/INSTALL b/INSTALL index 1a6442d9..aec1bda5 100644 --- a/INSTALL +++ b/INSTALL @@ -40,6 +40,13 @@ When running, the 'vdr' program writes status information into the system log file (/var/log/messages). You may want to watch these messages (tail -f /var/log/mesages) to see if there are any problems. +The program can be controlled via a network connection to its SVDRP +port ("Simple Video Disk Recorder Protocol"). By default, it listens +on port 2001 (use the --port=PORT option to change this). For details +about the SVDRP syntax see the source file 'svdrp.c'. + +Use "vdr --help" for a list of available command line options. + The video data directory: ------------------------- diff --git a/Makefile b/Makefile index cfa7ef7f..8fb678bc 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ # -# Makefile for the On Screen Menu of the Video Disk Recorder +# Makefile for the Video Disk Recorder # # See the main source file 'vdr.c' for copyright information and # how to reach the author. # -# $Id: Makefile 1.4 2000/06/24 15:09:30 kls Exp $ +# $Id: Makefile 1.5 2000/07/23 11:57:14 kls Exp $ -OBJS = config.o dvbapi.o interface.o menu.o osd.o recording.o remote.o tools.o vdr.o +OBJS = config.o dvbapi.o interface.o menu.o osd.o recording.o remote.o svdrp.o tools.o vdr.o ifndef REMOTE REMOTE = KBD @@ -28,9 +28,10 @@ dvbapi.o : dvbapi.c config.h dvbapi.h interface.h tools.h interface.o: interface.c config.h dvbapi.h interface.h remote.h tools.h menu.o : menu.c config.h dvbapi.h interface.h menu.h osd.h recording.h tools.h osd.o : osd.c config.h dvbapi.h interface.h osd.h tools.h -vdr.o : vdr.c config.h dvbapi.h interface.h menu.h osd.h recording.h tools.h +vdr.o : vdr.c config.h dvbapi.h interface.h menu.h osd.h recording.h svdrp.h tools.h recording.o: recording.c config.h dvbapi.h interface.h recording.h tools.h remote.o : remote.c remote.h tools.h +svdrp.o : svdrp.c svdrp.h config.h interface.h tools.h tools.o : tools.c tools.h vdr: $(OBJS) diff --git a/TODO b/TODO index 64b31b23..09e9a10b 100644 --- a/TODO +++ b/TODO @@ -8,3 +8,4 @@ TODO list for the Video Disk Recorder project commercial breaks). * Implement channel scanning. * Better support for encrypted channels. +* Implement remaining commands in SVDRP. diff --git a/config.c b/config.c index 872e4f21..72043c13 100644 --- a/config.c +++ b/config.c @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: config.c 1.12 2000/07/21 13:10:50 kls Exp $ + * $Id: config.c 1.13 2000/07/23 11:56:06 kls Exp $ */ #include "config.h" @@ -60,7 +60,7 @@ void cKeys::SetDummyValues(void) k->code = k->type + 1; // '+1' to avoid 0 } -bool cKeys::Load(char *FileName) +bool cKeys::Load(const char *FileName) { isyslog(LOG_INFO, "loading %s", FileName); bool result = false; @@ -175,6 +175,8 @@ void cKeys::Set(eKeys Key, unsigned int Code) // -- cChannel --------------------------------------------------------------- +char *cChannel::buffer = NULL; + cChannel::cChannel(void) { *name = 0; @@ -193,7 +195,18 @@ cChannel::cChannel(const cChannel *Channel) pnr = Channel ? Channel->pnr : 0; } -bool cChannel::Parse(char *s) +const char *cChannel::ToText(cChannel *Channel) +{ + asprintf(&buffer, "%s:%d:%c:%d:%d:%d:%d:%d:%d\n", Channel->name, Channel->frequency, Channel->polarization, Channel->diseqc, Channel->srate, Channel->vpid, Channel->apid, Channel->ca, Channel->pnr); + return buffer; +} + +const char *cChannel::ToText(void) +{ + return ToText(this); +} + +bool cChannel::Parse(const char *s) { char *buffer = NULL; if (9 == sscanf(s, "%a[^:]:%d:%c:%d:%d:%d:%d:%d:%d", &buffer, &frequency, &polarization, &diseqc, &srate, &vpid, &apid, &ca, &pnr)) { @@ -207,7 +220,7 @@ bool cChannel::Parse(char *s) bool cChannel::Save(FILE *f) { - return fprintf(f, "%s:%d:%c:%d:%d:%d:%d:%d:%d\n", name, frequency, polarization, diseqc, srate, vpid, apid, ca, pnr) > 0; + return fprintf(f, ToText()) > 0; } bool cChannel::Switch(cDvbApi *DvbApi) @@ -242,6 +255,8 @@ const char *cChannel::GetChannelName(int i) // -- cTimer ----------------------------------------------------------------- +char *cTimer::buffer = NULL; + cTimer::cTimer(bool Instant) { startTime = stopTime = 0; @@ -263,6 +278,17 @@ cTimer::cTimer(bool Instant) snprintf(file, sizeof(file), "@%s", cChannel::GetChannelName(CurrentChannel)); } +const char *cTimer::ToText(cTimer *Timer) +{ + asprintf(&buffer, "%d:%d:%s:%d:%d:%d:%d:%s\n", Timer->active, Timer->channel, PrintDay(Timer->day), Timer->start, Timer->stop, Timer->priority, Timer->lifetime, Timer->file); + return buffer; +} + +const char *cTimer::ToText(void) +{ + return ToText(this); +} + int cTimer::TimeToInt(int t) { return (t / 100 * 60 + t % 100) * 60; @@ -275,7 +301,7 @@ time_t cTimer::Day(time_t t) return mktime(&d); } -int cTimer::ParseDay(char *s) +int cTimer::ParseDay(const char *s) { char *tail; int d = strtol(s, &tail, 10); @@ -283,7 +309,7 @@ int cTimer::ParseDay(char *s) d = 0; if (tail == s) { if (strlen(s) == 7) { - for (char *p = s + 6; p >= s; p--) { + for (const char *p = s + 6; p >= s; p--) { d <<= 1; d |= (*p != '-'); } @@ -296,7 +322,7 @@ int cTimer::ParseDay(char *s) return d; } -char *cTimer::PrintDay(int d) +const char *cTimer::PrintDay(int d) { static char buffer[8]; if ((d & 0x80000000) != 0) { @@ -314,11 +340,12 @@ char *cTimer::PrintDay(int d) return buffer; } -bool cTimer::Parse(char *s) +bool cTimer::Parse(const char *s) { char *buffer1 = NULL; char *buffer2 = NULL; if (8 == sscanf(s, "%d:%d:%a[^:]:%d:%d:%d:%d:%a[^:\n]", &active, &channel, &buffer1, &start, &stop, &priority, &lifetime, &buffer2)) { + //TODO add more plausibility checks day = ParseDay(buffer1); strncpy(file, buffer2, MaxFileName - 1); file[strlen(buffer2)] = 0; @@ -331,7 +358,7 @@ bool cTimer::Parse(char *s) bool cTimer::Save(FILE *f) { - return fprintf(f, "%d:%d:%s:%d:%d:%d:%d:%s\n", active, channel, PrintDay(day), start, stop, priority, lifetime, file) > 0; + return fprintf(f, ToText()) > 0; } bool cTimer::IsSingleEvent(void) diff --git a/config.h b/config.h index d7f9f092..28d07edf 100644 --- a/config.h +++ b/config.h @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: config.h 1.9 2000/07/16 11:41:51 kls Exp $ + * $Id: config.h 1.10 2000/07/23 11:54:53 kls Exp $ */ #ifndef __CONFIG_H @@ -51,7 +51,7 @@ public: cKeys(void); void Clear(void); void SetDummyValues(void); - bool Load(char *FileName = NULL); + bool Load(const char *FileName = NULL); bool Save(void); unsigned int Encode(const char *Command); eKeys Get(unsigned int Code); @@ -59,6 +59,9 @@ public: }; class cChannel : public cListObject { +private: + static char *buffer; + static const char *ToText(cChannel *Channel); public: enum { MaxChannelName = 32 }; // 31 chars + terminating 0! char name[MaxChannelName]; @@ -72,7 +75,8 @@ public: int pnr; cChannel(void); cChannel(const cChannel *Channel); - bool Parse(char *s); + const char *ToText(void); + bool Parse(const char *s); bool Save(FILE *f); bool Switch(cDvbApi *DvbApi = NULL); static bool SwitchTo(int i, cDvbApi *DvbApi = NULL); @@ -82,6 +86,8 @@ public: class cTimer : public cListObject { private: time_t startTime, stopTime; + static char *buffer; + static const char *ToText(cTimer *Timer); public: enum { MaxFileName = 256 }; bool recording; @@ -95,7 +101,8 @@ public: int lifetime; char file[MaxFileName]; cTimer(bool Instant = false); - bool Parse(char *s); + const char *ToText(void); + bool Parse(const char *s); bool Save(FILE *f); bool IsSingleEvent(void); bool Matches(time_t t = 0); @@ -105,8 +112,8 @@ public: static cTimer *GetMatch(void); static int TimeToInt(int t); static time_t Day(time_t t); - static int ParseDay(char *s); - static char *PrintDay(int d); + static int ParseDay(const char *s); + static const char *PrintDay(int d); }; template class cConfig : public cList { @@ -118,7 +125,7 @@ private: cList::Clear(); } public: - bool Load(char *FileName) + bool Load(const char *FileName) { isyslog(LOG_INFO, "loading %s", FileName); bool result = true; diff --git a/svdrp.c b/svdrp.c new file mode 100644 index 00000000..c1347a31 --- /dev/null +++ b/svdrp.c @@ -0,0 +1,620 @@ +/* + * svdrp.c: Simple Video Disk Recorder Protocol + * + * See the main source file 'vdr.c' for copyright information and + * how to reach the author. + * + * The "Simple Video Disk Recorder Protocol" (SVDRP) was inspired + * by the "Simple Mail Transfer Protocol" (SMTP) and is fully ASCII + * text based. Therefore you can simply 'telnet' to your VDR port + * and interact with the Video Disk Recorder - or write a full featured + * graphical interface that sits on top of an SVDRP connection. + * + * $Id: svdrp.c 1.1 2000/07/23 14:55:03 kls Exp $ + */ + +#define _GNU_SOURCE + +#include "svdrp.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "interface.h" +#include "tools.h" + +// --- cSocket --------------------------------------------------------------- + +cSocket::cSocket(int Port, int Queue) +{ + port = Port; + sock = -1; +} + +cSocket::~cSocket() +{ + Close(); +} + +void cSocket::Close(void) +{ + if (sock >= 0) { + close(sock); + sock = -1; + } +} + +bool cSocket::Open(void) +{ + if (sock < 0) { + // create socket: + sock = socket(PF_INET, SOCK_STREAM, 0); + if (sock < 0) { + LOG_ERROR; + port = 0; + return false; + } + struct sockaddr_in name; + name.sin_family = AF_INET; + name.sin_port = htons(port); + name.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(sock, (struct sockaddr *)&name, sizeof(name)) < 0) { + LOG_ERROR; + Close(); + return false; + } + // make it non-blocking: + int oldflags = fcntl(sock, F_GETFL, 0); + if (oldflags < 0) { + LOG_ERROR; + return false; + } + oldflags |= O_NONBLOCK; + if (fcntl(sock, F_SETFL, oldflags) < 0) { + LOG_ERROR; + return false; + } + // listen to the socket: + if (listen(sock, queue) < 0) { + LOG_ERROR; + return false; + } + } + return true; +} + +int cSocket::Accept(void) +{ + if (Open()) { + struct sockaddr_in clientname; + uint size = sizeof(clientname); + int newsock = accept(sock, (struct sockaddr *)&clientname, &size); + if (newsock > 0) + isyslog(LOG_INFO, "connect from %s, port %hd", inet_ntoa(clientname.sin_addr), ntohs(clientname.sin_port)); + else if (errno != EINTR) + LOG_ERROR; + return newsock; + } + return -1; +} + +// --- cSVDRP ---------------------------------------------------------------- + +#define MAXCMDBUFFER 10000 +#define MAXHELPTOPIC 10 + +const char *HelpPages[] = { + "CHAN [ + | - | | ]\n" + " Switch channel up, down or to the given channel number or name.\n" + " Without option (or after successfully switching to the channel)\n" + " it returns the current channel number and name.", + "DELC \n" + " Delete channel.", + "DELT \n" + " Delete timer.", + "HELP [ ]\n" + " The HELP command gives help info.", + "LSTC [ | ]\n" + " List channels. Without option, all channels are listed. Otherwise\n" + " only the given channel is listed. If a name is given, all channels\n" + " containing the given string as part of their name are listed.", + "LSTT [ ]\n" + " List timers. Without option, all timers are listed. Otherwise\n" + " only the given timer is listed.", + "MODC \n" + " Modify a channel. Settings must be in the same format as returned\n" + " by the LSTC command.", + "MODT on | off | \n" + " Modify a timer. Settings must be in the same format as returned\n" + " by the LSTT command. The special keywords 'on' and 'off' can be\n" + " used to easily activate or deactivate a timer.", + "MOVC \n" + " Move a channel to a new position.", + "MOVT \n" + " Move a timer to a new position.", + "NEWC \n" + " Create a new channel. Settings must be in the same format as returned\n" + " by the LSTC command.", + "NEWT \n" + " Create a new timer. Settings must be in the same format as returned\n" + " by the LSTT command.", + "QUIT\n" + " Exit vdr (SVDRP).\n" + " You can also hit Ctrl-D to exit.", + NULL + }; + +/* SVDRP Reply Codes: + + 214 Help message + 220 VDR service ready + 221 VDR service closing transmission channel + 250 Requested VDR action okay, completed + 451 Requested action aborted: local error in processing + 500 Syntax error, command unrecognized + 501 Syntax error in parameters or arguments + 502 Command not implemented + 504 Command parameter not implemented + 550 Requested action not taken + 554 Transaction failed + +*/ + +const char *GetHelpTopic(const char *HelpPage) +{ + static char topic[MAXHELPTOPIC]; + const char *q = HelpPage; + while (*q) { + if (isspace(*q)) { + uint n = q - HelpPage; + if (n >= sizeof(topic)) + n = sizeof(topic) - 1; + strncpy(topic, HelpPage, n); + topic[n] = 0; + return topic; + } + q++; + } + return NULL; +} + +const char *GetHelpPage(const char *Cmd) +{ + const char **p = HelpPages; + while (*p) { + const char *t = GetHelpTopic(*p); + if (strcasecmp(Cmd, t) == 0) + return *p; + p++; + } + return NULL; +} + +cSVDRP::cSVDRP(int Port) +:socket(Port) +{ + filedes = -1; + isyslog(LOG_INFO, "SVDRP listening on port %d", Port); +} + +cSVDRP::~cSVDRP() +{ + Close(); +} + +void cSVDRP::Close(void) +{ + if (filedes >= 0) { + //TODO how can we get the *full* hostname? + char buffer[MAXCMDBUFFER]; + gethostname(buffer, sizeof(buffer)); + Reply(221, "%s closing connection", buffer); + isyslog(LOG_INFO, "closing connection"); //TODO store IP#??? + close(filedes); + filedes = -1; + } +} + +bool cSVDRP::Send(const char *s, int length) +{ + if (length < 0) + length = strlen(s); + int wbytes = write(filedes, s, length); + if (wbytes == length) + return true; + if (wbytes < 0) + LOG_ERROR; + else //XXX while...??? + esyslog(LOG_ERR, "Wrote %d bytes to client while expecting %d\n", wbytes, length); + return false; +} + +void cSVDRP::Reply(int Code, const char *fmt, ...) +{ + if (filedes >= 0) { + if (Code != 0) { + va_list ap; + va_start(ap, fmt); + char *buffer; + vasprintf(&buffer, fmt, ap); + char *nl = strchr(buffer, '\n'); + if (Code > 0 && nl && *(nl + 1)) // trailing newlines don't count! + Code = -Code; + char number[16]; + sprintf(number, "%03d%c", abs(Code), Code < 0 ? '-' : ' '); + const char *s = buffer; + while (s && *s) { + const char *n = strchr(s, '\n'); + if (!(Send(number) && Send(s, n ? n - s : -1) && Send("\r\n"))) { + Close(); + break; + } + s = n ? n + 1 : NULL; + } + delete buffer; + va_end(ap); + } + else { + Reply(451, "Zero return code - looks like a programming error!"); + esyslog(LOG_ERR, "SVDRP: zero return code!"); + } + } +} + +void cSVDRP::CmdChan(const char *Option) +{ + if (*Option) { + int n = -1; + if (isnumber(Option)) { + int o = strtol(Option, NULL, 10) - 1; + if (o >= 0 && o < Channels.Count()) + n = o; + } + else if (strcmp(Option, "-") == 0) { + n = CurrentChannel; + if (CurrentChannel > 0) + n--; + } + else if (strcmp(Option, "+") == 0) { + n = CurrentChannel; + if (CurrentChannel < Channels.Count() - 1) + n++; + } + else { + int i = 0; + cChannel *channel; + while ((channel = Channels.Get(i)) != NULL) { + if (strcasecmp(channel->name, Option) == 0) { + n = i; + break; + } + i++; + } + } + if (n < 0) { + Reply(501, "Undefined channel \"%s\"", Option); + return; + } + if (Interface.Recording()) { + Reply(550, "Can't switch channel, interface is recording"); + return; + } + cChannel *channel = Channels.Get(n); + if (channel) { + if (!channel->Switch()) { + Reply(554, "Error switching to channel \"%d\"", channel->Index() + 1); + return; + } + } + else { + Reply(550, "Unable to find channel \"%s\"", Option); + return; + } + } + cChannel *channel = Channels.Get(CurrentChannel); + if (channel) + Reply(250, "%d %s", CurrentChannel + 1, channel->name); + else + Reply(550, "Unable to find channel \"%d\"", CurrentChannel); +} + +void cSVDRP::CmdDelc(const char *Option) +{ + //TODO combine this with menu action (timers must be updated) + Reply(502, "DELC not yet implemented"); +} + +void cSVDRP::CmdDelt(const char *Option) +{ + if (*Option) { + if (isnumber(Option)) { + cTimer *timer = Timers.Get(strtol(Option, NULL, 10) - 1); + if (timer) { + if (!timer->recording) { + Timers.Del(timer); + Timers.Save(); + isyslog(LOG_INFO, "timer %s deleted", Option); + Reply(250, "Timer \"%s\" deleted", Option); + } + else + Reply(550, "Timer \"%s\" is recording", Option); + } + else + Reply(501, "Timer \"%s\" not defined", Option); + } + else + Reply(501, "Error in timer number \"%s\"", Option); + } + else + Reply(501, "Missing timer number"); +} + +void cSVDRP::CmdHelp(const char *Option) +{ + if (*Option) { + const char *hp = GetHelpPage(Option); + if (hp) + Reply(214, hp); + else { + Reply(504, "HELP topic \"%s\" unknown", Option); + return; + } + } + else { + Reply(-214, "This is VDR version 0.060"); //XXX dynamically insert version number + Reply(-214, "Topics:"); + const char **hp = HelpPages; + while (*hp) { + //TODO multi-column??? + const char *topic = GetHelpTopic(*hp); + if (topic) + Reply(-214, " %s", topic); + hp++; + } + Reply(-214, "To report bugs in the implementation send email to"); + Reply(-214, " vdr-bugs@cadsoft.de"); + } + Reply(214, "End of HELP info"); +} + +void cSVDRP::CmdLstc(const char *Option) +{ + if (*Option) { + if (isnumber(Option)) { + cChannel *channel = Channels.Get(strtol(Option, NULL, 10) - 1); + if (channel) + Reply(250, "%d %s", channel->Index() + 1, channel->ToText()); + else + Reply(501, "Channel \"%s\" not defined", Option); + } + else { + int i = 0; + cChannel *next = NULL; + while (i < Channels.Count()) { + cChannel *channel = Channels.Get(i); + if (channel) { + if (strcasestr(channel->name, Option)) { + if (next) + Reply(-250, "%d %s", next->Index() + 1, next->ToText()); + next = channel; + } + } + else { + Reply(501, "Channel \"%d\" not found", i + 1); + return; + } + i++; + } + if (next) + Reply(250, "%d %s", next->Index() + 1, next->ToText()); + } + } + else { + for (int i = 0; i < Channels.Count(); i++) { + cChannel *channel = Channels.Get(i); + if (channel) + Reply(i < Channels.Count() - 1 ? -250 : 250, "%d %s", channel->Index() + 1, channel->ToText()); + else + Reply(501, "Channel \"%d\" not found", i + 1); + } + } +} + +void cSVDRP::CmdLstt(const char *Option) +{ + if (*Option) { + if (isnumber(Option)) { + cTimer *timer = Timers.Get(strtol(Option, NULL, 10) - 1); + if (timer) + Reply(250, "%d %s", timer->Index() + 1, timer->ToText()); + else + Reply(501, "Timer \"%s\" not defined", Option); + } + else + Reply(501, "Error in timer number \"%s\"", Option); + } + else { + for (int i = 0; i < Timers.Count(); i++) { + cTimer *timer = Timers.Get(i); + if (timer) + Reply(i < Timers.Count() - 1 ? -250 : 250, "%d %s", timer->Index() + 1, timer->ToText()); + else + Reply(501, "Timer \"%d\" not found", i + 1); + } + } +} + +void cSVDRP::CmdModc(const char *Option) +{ + if (*Option) { + char *tail; + int n = strtol(Option, &tail, 10); + if (tail && tail != Option) { + tail = skipspace(tail); + cChannel *channel = Channels.Get(n - 1); + if (channel) { + cChannel c = *channel; + if (!c.Parse(tail)) { + Reply(501, "Error in channel settings"); + return; + } + *channel = c; + Channels.Save(); + isyslog(LOG_INFO, "channel %d modified", channel->Index() + 1); + Reply(250, "%d %s", channel->Index() + 1, channel->ToText()); + } + else + Reply(501, "Channel \"%d\" not defined", n); + } + else + Reply(501, "Error in channel number"); + } + else + Reply(501, "Missing channel settings"); +} + +void cSVDRP::CmdModt(const char *Option) +{ + if (*Option) { + char *tail; + int n = strtol(Option, &tail, 10); + if (tail && tail != Option) { + tail = skipspace(tail); + cTimer *timer = Timers.Get(n - 1); + if (timer) { + cTimer t = *timer; + if (strcasecmp(tail, "ON") == 0) + t.active = 1; + else if (strcasecmp(tail, "OFF") == 0) + t.active = 0; + else if (!t.Parse(tail)) { + Reply(501, "Error in timer settings"); + return; + } + *timer = t; + Timers.Save(); + isyslog(LOG_INFO, "timer %d modified (%s)", timer->Index() + 1, timer->active ? "active" : "inactive"); + Reply(250, "%d %s", timer->Index() + 1, timer->ToText()); + } + else + Reply(501, "Timer \"%d\" not defined", n); + } + else + Reply(501, "Error in timer number"); + } + else + Reply(501, "Missing timer settings"); +} + +void cSVDRP::CmdMovc(const char *Option) +{ + //TODO combine this with menu action (timers must be updated) + Reply(502, "MOVC not yet implemented"); +} + +void cSVDRP::CmdMovt(const char *Option) +{ + //TODO combine this with menu action + Reply(502, "MOVT not yet implemented"); +} + +void cSVDRP::CmdNewc(const char *Option) +{ + if (*Option) { + cChannel *channel = new cChannel; + if (channel->Parse(Option)) { + Channels.Add(channel); + Channels.Save(); + isyslog(LOG_INFO, "channel %d added", channel->Index() + 1); + Reply(250, "%d %s", channel->Index() + 1, channel->ToText()); + } + else + Reply(501, "Error in channel settings"); + } + else + Reply(501, "Missing channel settings"); +} + +void cSVDRP::CmdNewt(const char *Option) +{ + if (*Option) { + cTimer *timer = new cTimer; + if (timer->Parse(Option)) { + Timers.Add(timer); + Timers.Save(); + isyslog(LOG_INFO, "timer %d added", timer->Index() + 1); + Reply(250, "%d %s", timer->Index() + 1, timer->ToText()); + } + else + Reply(501, "Error in timer settings"); + } + else + Reply(501, "Missing timer settings"); +} + +#define CMD(c) (strcasecmp(Cmd, c) == 0) + +void cSVDRP::Execute(char *Cmd) +{ + // skip leading whitespace: + Cmd = skipspace(Cmd); + // find the end of the command word: + char *s = Cmd; + while (*s && !isspace(*s)) + s++; + *s++ = 0; + if (CMD("CHAN")) CmdChan(s); + else if (CMD("DELC")) CmdDelc(s); + else if (CMD("DELT")) CmdDelt(s); + else if (CMD("HELP")) CmdHelp(s); + else if (CMD("LSTC")) CmdLstc(s); + else if (CMD("LSTT")) CmdLstt(s); + else if (CMD("MODC")) CmdModc(s); + else if (CMD("MODT")) CmdModt(s); + else if (CMD("MOVC")) CmdMovc(s); + else if (CMD("MOVT")) CmdMovt(s); + else if (CMD("NEWC")) CmdNewc(s); + else if (CMD("NEWT")) CmdNewt(s); + else if (CMD("QUIT") + || CMD("\x04")) Close(); + else Reply(500, "Command unrecognized: \"%s\"", Cmd); +} + +void cSVDRP::Process(void) +{ + bool SendGreeting = filedes < 0; + + if (filedes >= 0 || (filedes = socket.Accept()) >= 0) { + char buffer[MAXCMDBUFFER]; + if (SendGreeting) { + //TODO how can we get the *full* hostname? + gethostname(buffer, sizeof(buffer)); + time_t now = time(NULL); + Reply(220, "%s SVDRP VideoDiskRecorder 0.060; %s", buffer, ctime(&now));//XXX dynamically insert version number + } + int rbytes = readstring(filedes, buffer, sizeof(buffer) - 1); + if (rbytes > 0) { + //XXX overflow check??? + // strip trailing whitespace: + while (rbytes > 0 && strchr(" \t\r\n", buffer[rbytes - 1])) + buffer[--rbytes] = 0; + // make sure the string is terminated: + buffer[rbytes] = 0; + // showtime! + Execute(buffer); + } + else if (rbytes < 0) + Close(); + } +} + +//TODO timeout??? +//TODO more than one connection??? diff --git a/svdrp.h b/svdrp.h new file mode 100644 index 00000000..c6385424 --- /dev/null +++ b/svdrp.h @@ -0,0 +1,52 @@ +/* + * svdrp.h: Simple Video Disk Recorder Protocol + * + * See the main source file 'vdr.c' for copyright information and + * how to reach the author. + * + * $Id: svdrp.h 1.1 2000/07/23 14:49:30 kls Exp $ + */ + +#ifndef __SVDRP_H +#define __SVDRP_H + +class cSocket { +private: + int port; + int sock; + int queue; + void Close(void); +public: + cSocket(int Port, int Queue = 1); + ~cSocket(); + bool Open(void); + int Accept(void); + }; + +class cSVDRP { +private: + cSocket socket; + int filedes; + void Close(void); + bool Send(const char *s, int length = -1); + void Reply(int Code, const char *fmt, ...); + void CmdChan(const char *Option); + void CmdDelc(const char *Option); + void CmdDelt(const char *Option); + void CmdHelp(const char *Option); + void CmdLstc(const char *Option); + void CmdLstt(const char *Option); + void CmdModc(const char *Option); + void CmdModt(const char *Option); + void CmdMovc(const char *Option); + void CmdMovt(const char *Option); + void CmdNewc(const char *Option); + void CmdNewt(const char *Option); + void Execute(char *Cmd); +public: + cSVDRP(int Port); + ~cSVDRP(); + void Process(void); + }; + +#endif //__SVDRP_H diff --git a/tools.c b/tools.c index b2c95f37..998d91b3 100644 --- a/tools.c +++ b/tools.c @@ -4,11 +4,12 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: tools.c 1.9 2000/07/16 14:14:44 kls Exp $ + * $Id: tools.c 1.10 2000/07/23 13:16:54 kls Exp $ */ #define _GNU_SOURCE #include "tools.h" +#include #include #include #include @@ -58,6 +59,26 @@ bool readint(int filedes, int &n) return DataAvailable(filedes) && read(filedes, &n, sizeof(n)) == sizeof(n); } +int readstring(int filedes, char *buffer, int size, bool wait = false) +{ + int rbytes = 0; + + while (DataAvailable(filedes, wait)) { + int n = read(filedes, buffer + rbytes, size - rbytes); + if (n == 0) + break; // EOF + if (n < 0) { + LOG_ERROR; + break; + } + rbytes += n; + if (rbytes == size) + break; + wait = false; + } + return rbytes; +} + void purge(int filedes) { while (DataAvailable(filedes)) @@ -88,6 +109,13 @@ char *strreplace(char *s, char c1, char c2) return s; } +char *skipspace(char *s) +{ + while (*s && isspace(*s)) + s++; + return s; +} + int time_ms(void) { static time_t t0 = 0; @@ -107,6 +135,16 @@ void delay_ms(int ms) ; } +bool isnumber(const char *s) +{ + while (*s) { + if (!isdigit(*s)) + return false; + s++; + } + return true; +} + bool MakeDirs(const char *FileName, bool IsDirectory) { bool result = true; diff --git a/tools.h b/tools.h index 0e26ec21..ecf42be6 100644 --- a/tools.h +++ b/tools.h @@ -4,7 +4,7 @@ * See the main source file 'vdr.c' for copyright information and * how to reach the author. * - * $Id: tools.h 1.9 2000/07/16 14:11:34 kls Exp $ + * $Id: tools.h 1.10 2000/07/23 13:16:37 kls Exp $ */ #ifndef __TOOLS_H @@ -35,11 +35,14 @@ void writechar(int filedes, char c); void writeint(int filedes, int n); char readchar(int filedes); bool readint(int filedes, int &n); +int readstring(int filedes, char *buffer, int size, bool wait = false); void purge(int filedes); char *readline(FILE *f); char *strreplace(char *s, char c1, char c2); +char *skipspace(char *s); int time_ms(void); void delay_ms(int ms); +bool isnumber(const char *s); bool MakeDirs(const char *FileName, bool IsDirectory = false); bool RemoveFileOrDir(const char *FileName); bool CheckProcess(pid_t pid); diff --git a/vdr.c b/vdr.c index a065bfd2..06e9ea88 100644 --- a/vdr.c +++ b/vdr.c @@ -22,15 +22,18 @@ * * The project's page is at http://www.cadsoft.de/people/kls/vdr * - * $Id: vdr.c 1.21 2000/07/15 16:26:57 kls Exp $ + * $Id: vdr.c 1.22 2000/07/23 14:53:22 kls Exp $ */ +#include #include +#include #include "config.h" #include "dvbapi.h" #include "interface.h" #include "menu.h" #include "recording.h" +#include "svdrp.h" #include "tools.h" #ifdef REMOTE_KBD @@ -50,12 +53,53 @@ void SignalHandler(int signum) int main(int argc, char *argv[]) { + // Command line options: + +#define DEFAULTSVDRPPORT 2001 + + int SVDRPport = DEFAULTSVDRPPORT; + + static struct option long_options[] = { + { "help", no_argument, NULL, 'h' }, + { "port", required_argument, NULL, 'p' }, + { 0 } + }; + + int c; + int option_index = 0; + while ((c = getopt_long(argc, argv, "hp:", long_options, &option_index)) != -1) { + switch (c) { + case 'h': printf("Usage: vdr [OPTION]\n\n" + " -h, --help display this help and exit\n" + " -p PORT, --port=PORT use PORT for SVDRP ('0' turns off SVDRP)\n" + "\n" + "Report bugs to \n" + ); + return 0; + break; + case 'p': if (isnumber(optarg)) + SVDRPport = strtol(optarg, NULL, 10); + else { + fprintf(stderr, "vdr: invalid port number: %s\n", optarg); + return 1; + } + break; + default: abort(); + } + } + + // Log file: + openlog("vdr", LOG_PID | LOG_CONS, LOG_USER); isyslog(LOG_INFO, "started"); + // DVB interfaces: + if (!cDvbApi::Init()) return 1; + // Configuration data: + Channels.Load("channels.conf"); Timers.Load("timers.conf"); #ifdef REMOTE_LIRC @@ -68,10 +112,15 @@ int main(int argc, char *argv[]) cChannel::SwitchTo(CurrentChannel); + // Signal handlers: + if (signal(SIGHUP, SignalHandler) == SIG_IGN) signal(SIGHUP, SIG_IGN); if (signal(SIGINT, SignalHandler) == SIG_IGN) signal(SIGINT, SIG_IGN); if (signal(SIGTERM, SignalHandler) == SIG_IGN) signal(SIGTERM, SIG_IGN); + // Main program loop: + + cSVDRP *SVDRP = SVDRPport ? new cSVDRP(SVDRPport) : NULL; cMenuMain *Menu = NULL; cReplayControl *ReplayControl = NULL; int dcTime = 0, dcNumber = 0; @@ -157,10 +206,13 @@ int main(int argc, char *argv[]) default: break; } } + if (SVDRP) + SVDRP->Process();//TODO lock menu vs. SVDRP? } isyslog(LOG_INFO, "caught signal %d", Interrupted); delete Menu; delete ReplayControl; + delete SVDRP; cDvbApi::Cleanup(); isyslog(LOG_INFO, "exiting"); closelog();