From 52514313fbb740517a65da615de18d3e6e9e151a Mon Sep 17 00:00:00 2001
From: Klaus Schmidinger <vdr@tvdr.de>
Date: Sun, 23 Jul 2000 15:01:31 +0200
Subject: Implemented SVDRP

---
 HISTORY  |   5 +-
 INSTALL  |   7 +
 Makefile |   9 +-
 TODO     |   1 +
 config.c |  45 ++++-
 config.h |  21 ++-
 svdrp.c  | 620 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 svdrp.h  |  52 ++++++
 tools.c  |  40 ++++-
 tools.h  |   5 +-
 vdr.c    |  54 +++++-
 11 files changed, 835 insertions(+), 24 deletions(-)
 create mode 100644 svdrp.c
 create mode 100644 svdrp.h

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 T> class cConfig : public cList<T> {
@@ -118,7 +125,7 @@ private:
     cList<T>::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 <arpa/inet.h>
+#include <ctype.h>
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <unistd.h>
+#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 [ + | - | <number> | <name> ]\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 <number>\n"
+  "    Delete channel.",
+  "DELT <number>\n"
+  "    Delete timer.",
+  "HELP [ <topic> ]\n"
+  "    The HELP command gives help info.",
+  "LSTC [ <number> | <name> ]\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 [ <number> ]\n"
+  "    List timers. Without option, all timers are listed. Otherwise\n"
+  "    only the given timer is listed.",
+  "MODC <number> <settings>\n"
+  "    Modify a channel. Settings must be in the same format as returned\n"
+  "    by the LSTC command.",
+  "MODT <number> on | off | <settings>\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 <number> <to>\n"
+  "    Move a channel to a new position.",
+  "MOVT <number> <to>\n"
+  "    Move a timer to a new position.",
+  "NEWC <settings>\n"
+  "    Create a new channel. Settings must be in the same format as returned\n"
+  "    by the LSTC command.",
+  "NEWT <settings>\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 <ctype.h>
 #include <dirent.h>
 #include <errno.h>
 #include <signal.h>
@@ -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 <getopt.h>
 #include <signal.h>
+#include <stdlib.h>
 #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 <vdr-bugs@cadsoft.de>\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();
-- 
cgit v1.2.3