/* * vdr.c: Video Disk Recorder main program * * Copyright (C) 2000-2018 Klaus Schmidinger * * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * Or, point your browser to http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * * The author can be reached at vdr@tvdr.de * * The project's page is at http://www.tvdr.de * * $Id: vdr.c 5.6 2021/05/21 12:43:18 kls Exp $ */ #include #include #include #include #include #include #include #include #include #ifdef SDNOTIFY #include #endif #include #include #include "args.h" #include "audio.h" #include "channels.h" #include "config.h" #include "cutter.h" #include "device.h" #include "diseqc.h" #include "dvbdevice.h" #include "eitscan.h" #include "epg.h" #include "i18n.h" #include "interface.h" #include "keys.h" #include "libsi/si.h" #include "lirc.h" #include "menu.h" #include "osdbase.h" #include "plugin.h" #include "recording.h" #include "shutdown.h" #include "skinclassic.h" #include "skinlcars.h" #include "skinsttng.h" #include "sourceparams.h" #include "sources.h" #include "status.h" #include "svdrp.h" #include "themes.h" #include "timers.h" #include "tools.h" #include "transfer.h" #include "videodir.h" #define MINCHANNELWAIT 10 // seconds to wait between failed channel switchings #define ACTIVITYTIMEOUT 60 // seconds before starting housekeeping #define SHUTDOWNWAIT 300 // seconds to wait in user prompt before automatic shutdown #define SHUTDOWNRETRY 360 // seconds before trying again to shut down #define SHUTDOWNFORCEPROMPT 5 // seconds to wait in user prompt to allow forcing shutdown #define SHUTDOWNCANCELPROMPT 5 // seconds to wait in user prompt to allow canceling shutdown #define RESTARTCANCELPROMPT 5 // seconds to wait in user prompt before restarting on SIGHUP #define MANUALSTART 600 // seconds the next timer must be in the future to assume manual start #define CHANNELSAVEDELTA 600 // seconds before saving channels.conf after automatic modifications #define DEVICEREADYTIMEOUT 30 // seconds to wait until all devices are ready #define MENUTIMEOUT 120 // seconds of user inactivity after which an OSD display is closed #define TIMERCHECKDELTA 10 // seconds between checks for timers that need to see their channel #define TIMERDEVICETIMEOUT 8 // seconds before a device used for timer check may be reused #define TIMERLOOKAHEADTIME 60 // seconds before a non-VPS timer starts and the channel is switched if possible #define VPSLOOKAHEADTIME 24 // hours within which VPS timers will make sure their events are up to date #define VPSUPTODATETIME 3600 // seconds before the event or schedule of a VPS timer needs to be refreshed #define EXIT(v) { ShutdownHandler.Exit(v); goto Exit; } static int LastSignal = 0; static bool SetUser(const char *User, bool UserDump) { if (User) { struct passwd *user = isnumber(User) ? getpwuid(atoi(User)) : getpwnam(User); if (!user) { fprintf(stderr, "vdr: unknown user: '%s'\n", User); return false; } if (setgid(user->pw_gid) < 0) { fprintf(stderr, "vdr: cannot set group id %u: %s\n", (unsigned int)user->pw_gid, strerror(errno)); return false; } if (initgroups(user->pw_name, user->pw_gid) < 0) { fprintf(stderr, "vdr: cannot set supplemental group ids for user %s: %s\n", user->pw_name, strerror(errno)); return false; } if (setuid(user->pw_uid) < 0) { fprintf(stderr, "vdr: cannot set user id %u: %s\n", (unsigned int)user->pw_uid, strerror(errno)); return false; } if (UserDump && prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0) fprintf(stderr, "vdr: warning - cannot set dumpable: %s\n", strerror(errno)); setenv("HOME", user->pw_dir, 1); setenv("USER", user->pw_name, 1); setenv("LOGNAME", user->pw_name, 1); setenv("SHELL", user->pw_shell, 1); } return true; } static bool DropCaps(void) { // drop all capabilities except selected ones cap_t caps_all = cap_get_proc(); if (!caps_all) { fprintf(stderr, "vdr: cap_get_proc failed: %s\n", strerror(errno)); return false; } cap_flag_value_t cap_flag_value; if (cap_get_flag(caps_all, CAP_SYS_TIME, CAP_PERMITTED , &cap_flag_value)) { fprintf(stderr, "vdr: cap_get_flag failed: %s\n", strerror(errno)); return false; } cap_t caps; if (cap_flag_value == CAP_SET) caps = cap_from_text("= cap_sys_nice,cap_sys_time,cap_net_raw=ep"); else { fprintf(stdout,"vdr: OS does not support cap_sys_time\n"); caps = cap_from_text("= cap_sys_nice,cap_net_raw=ep"); } if (!caps) { fprintf(stderr, "vdr: cap_from_text failed: %s\n", strerror(errno)); return false; } if (cap_set_proc(caps) == -1) { fprintf(stderr, "vdr: cap_set_proc failed: %s\n", strerror(errno)); cap_free(caps); return false; } cap_free(caps); return true; } static bool SetKeepCaps(bool On) { // set keeping capabilities during setuid() on/off if (prctl(PR_SET_KEEPCAPS, On ? 1 : 0, 0, 0, 0) != 0) { fprintf(stderr, "vdr: prctl failed\n"); return false; } return true; } static void SignalHandler(int signum) { switch (signum) { case SIGPIPE: break; case SIGHUP: LastSignal = signum; break; default: LastSignal = signum; Interface->Interrupt(); ShutdownHandler.Exit(0); } signal(signum, SignalHandler); } static void Watchdog(int signum) { // Something terrible must have happened that prevented the 'alarm()' from // being called in time, so let's get out of here: esyslog("PANIC: watchdog timer expired - exiting!"); #ifdef SDNOTIFY sd_notify(0, "STOPPING=1\nSTATUS=PANIC"); #endif exit(1); } int main(int argc, char *argv[]) { // Save terminal settings: struct termios savedTm; bool HasStdin = (tcgetpgrp(STDIN_FILENO) == getpid() || getppid() != (pid_t)1) && tcgetattr(STDIN_FILENO, &savedTm) == 0; // Initiate locale: setlocale(LC_ALL, ""); // Command line options: #define dd(a, b) (*a ? a : b) #define DEFAULTSVDRPPORT 6419 #define DEFAULTWATCHDOG 0 // seconds #define DEFAULTVIDEODIR VIDEODIR #define DEFAULTCONFDIR dd(CONFDIR, VideoDirectory) #define DEFAULTARGSDIR dd(ARGSDIR, "/etc/vdr/conf.d") #define DEFAULTCACHEDIR dd(CACHEDIR, VideoDirectory) #define DEFAULTRESDIR dd(RESDIR, ConfigDirectory) #define DEFAULTPLUGINDIR PLUGINDIR #define DEFAULTLOCDIR LOCDIR #define DEFAULTEPGDATAFILENAME "epg.data" bool StartedAsRoot = false; const char *VdrUser = NULL; bool UserDump = false; int SVDRPport = DEFAULTSVDRPPORT; const char *AudioCommand = NULL; const char *VideoDirectory = DEFAULTVIDEODIR; const char *ConfigDirectory = NULL; const char *CacheDirectory = NULL; const char *ResourceDirectory = NULL; const char *LocaleDirectory = DEFAULTLOCDIR; const char *EpgDataFileName = DEFAULTEPGDATAFILENAME; bool DisplayHelp = false; bool DisplayVersion = false; bool DaemonMode = false; int SysLogTarget = LOG_USER; bool MuteAudio = false; int WatchdogTimeout = DEFAULTWATCHDOG; const char *Terminal = NULL; const char *OverrideCharacterTable = NULL; bool UseKbd = true; const char *LircDevice = NULL; #if !defined(REMOTE_KBD) UseKbd = false; #endif #if defined(REMOTE_LIRC) LircDevice = LIRC_DEVICE; #endif #if defined(VDR_USER) VdrUser = VDR_USER; #endif #ifdef SDNOTIFY time_t SdWatchdog = 0; int SdWatchdogTimeout = 0; #endif cArgs *Args = NULL; if (argc == 1) { Args = new cArgs(argv[0]); if (Args->ReadDirectory(DEFAULTARGSDIR)) { argc = Args->GetArgc(); argv = Args->GetArgv(); } } cVideoDirectory::SetName(VideoDirectory); cPluginManager PluginManager(DEFAULTPLUGINDIR); static struct option long_options[] = { { "audio", required_argument, NULL, 'a' }, { "cachedir", required_argument, NULL, 'c' | 0x100 }, { "chartab", required_argument, NULL, 'c' | 0x200 }, { "config", required_argument, NULL, 'c' }, { "daemon", no_argument, NULL, 'd' }, { "device", required_argument, NULL, 'D' }, { "dirnames", required_argument, NULL, 'd' | 0x100 }, { "edit", required_argument, NULL, 'e' | 0x100 }, { "epgfile", required_argument, NULL, 'E' }, { "filesize", required_argument, NULL, 'f' | 0x100 }, { "genindex", required_argument, NULL, 'g' | 0x100 }, { "grab", required_argument, NULL, 'g' }, { "help", no_argument, NULL, 'h' }, { "instance", required_argument, NULL, 'i' }, { "lib", required_argument, NULL, 'L' }, { "lirc", optional_argument, NULL, 'l' | 0x100 }, { "localedir",required_argument, NULL, 'l' | 0x200 }, { "log", required_argument, NULL, 'l' }, { "mute", no_argument, NULL, 'm' }, { "no-kbd", no_argument, NULL, 'n' | 0x100 }, { "plugin", required_argument, NULL, 'P' }, { "port", required_argument, NULL, 'p' }, { "record", required_argument, NULL, 'r' }, { "resdir", required_argument, NULL, 'r' | 0x100 }, { "showargs", optional_argument, NULL, 's' | 0x200 }, { "shutdown", required_argument, NULL, 's' }, { "split", no_argument, NULL, 's' | 0x100 }, { "terminal", required_argument, NULL, 't' }, { "updindex", required_argument, NULL, 'u' | 0x200 }, { "user", required_argument, NULL, 'u' }, { "userdump", no_argument, NULL, 'u' | 0x100 }, { "version", no_argument, NULL, 'V' }, { "vfat", no_argument, NULL, 'v' | 0x100 }, { "video", required_argument, NULL, 'v' }, { "watchdog", required_argument, NULL, 'w' }, { NULL, no_argument, NULL, 0 } }; int c; while ((c = getopt_long(argc, argv, "a:c:dD:e:E:g:hi:l:L:mp:P:r:s:t:u:v:Vw:", long_options, NULL)) != -1) { switch (c) { case 'a': AudioCommand = optarg; break; case 'c' | 0x100: CacheDirectory = optarg; break; case 'c' | 0x200: OverrideCharacterTable = optarg; break; case 'c': ConfigDirectory = optarg; break; case 'd': DaemonMode = true; break; case 'D': if (*optarg == '-') { cDvbDevice::useDvbDevices = false; break; } if (isnumber(optarg)) { int n = atoi(optarg); if (0 <= n && n < MAXDEVICES) { cDevice::SetUseDevice(n); break; } } fprintf(stderr, "vdr: invalid DVB device number: %s\n", optarg); return 2; case 'd' | 0x100: { char *s = optarg; if (*s != ',') { int n = strtol(s, &s, 10); if (n <= 0 || n >= PATH_MAX) { // PATH_MAX includes the terminating 0 fprintf(stderr, "vdr: invalid directory path length: %s\n", optarg); return 2; } DirectoryPathMax = n; if (!*s) break; if (*s != ',') { fprintf(stderr, "vdr: invalid delimiter: %s\n", optarg); return 2; } } s++; if (!*s) break; if (*s != ',') { int n = strtol(s, &s, 10); if (n <= 0 || n > NAME_MAX) { // NAME_MAX excludes the terminating 0 fprintf(stderr, "vdr: invalid directory name length: %s\n", optarg); return 2; } DirectoryNameMax = n; if (!*s) break; if (*s != ',') { fprintf(stderr, "vdr: invalid delimiter: %s\n", optarg); return 2; } } s++; if (!*s) break; int n = strtol(s, &s, 10); if (n != 0 && n != 1) { fprintf(stderr, "vdr: invalid directory encoding: %s\n", optarg); return 2; } DirectoryEncoding = n; if (*s) { fprintf(stderr, "vdr: unexpected data: %s\n", optarg); return 2; } } break; case 'e' | 0x100: return CutRecording(optarg) ? 0 : 2; case 'E': EpgDataFileName = (*optarg != '-' ? optarg : NULL); break; case 'f' | 0x100: Setup.MaxVideoFileSize = StrToNum(optarg) / MEGABYTE(1); if (Setup.MaxVideoFileSize < MINVIDEOFILESIZE) Setup.MaxVideoFileSize = MINVIDEOFILESIZE; if (Setup.MaxVideoFileSize > MAXVIDEOFILESIZETS) Setup.MaxVideoFileSize = MAXVIDEOFILESIZETS; break; case 'g' | 0x100: return GenerateIndex(optarg) ? 0 : 2; case 'g': SetSVDRPGrabImageDir(*optarg != '-' ? optarg : NULL); break; case 'h': DisplayHelp = true; break; case 'i': if (isnumber(optarg)) { InstanceId = atoi(optarg); if (InstanceId >= 0) break; } fprintf(stderr, "vdr: invalid instance id: %s\n", optarg); return 2; case 'l': { char *p = strchr(optarg, '.'); if (p) *p = 0; if (isnumber(optarg)) { int l = atoi(optarg); if (0 <= l && l <= 3) { SysLogLevel = l; if (!p) break; *p = '.'; if (isnumber(p + 1)) { int l = atoi(p + 1); if (0 <= l && l <= 7) { int targets[] = { LOG_LOCAL0, LOG_LOCAL1, LOG_LOCAL2, LOG_LOCAL3, LOG_LOCAL4, LOG_LOCAL5, LOG_LOCAL6, LOG_LOCAL7 }; SysLogTarget = targets[l]; break; } } } } if (p) *p = '.'; fprintf(stderr, "vdr: invalid log level: %s\n", optarg); return 2; } case 'L': if (access(optarg, R_OK | X_OK) == 0) PluginManager.SetDirectory(optarg); else { fprintf(stderr, "vdr: can't access plugin directory: %s\n", optarg); return 2; } break; case 'l' | 0x100: LircDevice = optarg ? optarg : LIRC_DEVICE; break; case 'l' | 0x200: if (access(optarg, R_OK | X_OK) == 0) LocaleDirectory = optarg; else { fprintf(stderr, "vdr: can't access locale directory: %s\n", optarg); return 2; } break; case 'm': MuteAudio = true; break; case 'n' | 0x100: UseKbd = false; break; case 'p': if (isnumber(optarg)) SVDRPport = atoi(optarg); else { fprintf(stderr, "vdr: invalid port number: %s\n", optarg); return 2; } break; case 'P': PluginManager.AddPlugin(optarg); break; case 'r': cRecordingUserCommand::SetCommand(optarg); break; case 'r' | 0x100: ResourceDirectory = optarg; break; case 's': ShutdownHandler.SetShutdownCommand(optarg); break; case 's' | 0x100: Setup.SplitEditedFiles = 1; break; case 's' | 0x200: { const char *ArgsDir = optarg ? optarg : DEFAULTARGSDIR; cArgs Args(argv[0]); if (!Args.ReadDirectory(ArgsDir)) { fprintf(stderr, "vdr: can't read arguments from directory: %s\n", ArgsDir); return 2; } int c = Args.GetArgc(); char **v = Args.GetArgv(); for (int i = 1; i < c; i++) printf("%s\n", v[i]); return 0; } case 't': Terminal = optarg; if (access(Terminal, R_OK | W_OK) < 0) { fprintf(stderr, "vdr: can't access terminal: %s\n", Terminal); return 2; } break; case 'u': if (*optarg) VdrUser = optarg; break; case 'u' | 0x100: UserDump = true; break; case 'u' | 0x200: return GenerateIndex(optarg, true) ? 0 : 2; case 'V': DisplayVersion = true; break; case 'v' | 0x100: DirectoryPathMax = 250; DirectoryNameMax = 40; DirectoryEncoding = true; break; case 'v': VideoDirectory = optarg; while (optarg && *optarg && optarg[strlen(optarg) - 1] == '/') optarg[strlen(optarg) - 1] = 0; cVideoDirectory::SetName(VideoDirectory); break; case 'w': if (isnumber(optarg)) { int t = atoi(optarg); if (t >= 0) { WatchdogTimeout = t; break; } } fprintf(stderr, "vdr: invalid watchdog timeout: %s\n", optarg); return 2; default: return 2; } } // Set user id in case we were started as root: if (VdrUser && geteuid() == 0) { StartedAsRoot = true; if (strcmp(VdrUser, "root") && strcmp(VdrUser, "0")) { if (!SetKeepCaps(true)) return 2; if (!SetUser(VdrUser, UserDump)) return 2; if (!SetKeepCaps(false)) return 2; if (!DropCaps()) return 2; } } // Help and version info: if (DisplayHelp || DisplayVersion) { if (!PluginManager.HasPlugins()) PluginManager.AddPlugin("*"); // adds all available plugins PluginManager.LoadPlugins(); if (DisplayHelp) { printf("Usage: vdr [OPTIONS]\n\n" // for easier orientation, this is column 80| " -a CMD, --audio=CMD send Dolby Digital audio to stdin of command CMD\n" " --cachedir=DIR save cache files in DIR (default: %s)\n" " --chartab=CHARACTER_TABLE\n" " set the character table to use for strings in the\n" " DVB data stream that don't begin with a character\n" " table indicator, but don't use the standard default\n" " character table (for instance ISO-8859-9)\n" " -c DIR, --config=DIR read config files from DIR (default: %s)\n" " -d, --daemon run in daemon mode\n" " -D NUM, --device=NUM use only the given DVB device (NUM = 0, 1, 2...)\n" " there may be several -D options (default: all DVB\n" " devices will be used); if -D- is given, no DVB\n" " devices will be used at all, independent of any\n" " other -D options\n" " --dirnames=PATH[,NAME[,ENC]]\n" " set the maximum directory path length to PATH\n" " (default: %d); if NAME is also given, it defines\n" " the maximum directory name length (default: %d);\n" " the optional ENC can be 0 or 1, and controls whether\n" " special characters in directory names are encoded as\n" " hex values (default: 0); if PATH or NAME are left\n" " empty (as in \",,1\" to only set ENC), the defaults\n" " apply\n" " --edit=REC cut recording REC and exit\n" " -E FILE, --epgfile=FILE write the EPG data into the given FILE (default is\n" " '%s' in the cache directory)\n" " '-E-' disables this\n" " if FILE is a directory, the default EPG file will be\n" " created in that directory\n" " --filesize=SIZE limit video files to SIZE bytes (default is %dM)\n" " only useful in conjunction with --edit\n" " --genindex=REC generate index for recording REC and exit\n" " -g DIR, --grab=DIR write images from the SVDRP command GRAB into the\n" " given DIR; DIR must be the full path name of an\n" " existing directory, without any \"..\", double '/'\n" " or symlinks (default: none, same as -g-)\n" " -h, --help print this help and exit\n" " -i ID, --instance=ID use ID as the id of this VDR instance (default: 0)\n" " -l LEVEL, --log=LEVEL set log level (default: 3)\n" " 0 = no logging, 1 = errors only,\n" " 2 = errors and info, 3 = errors, info and debug\n" " if logging should be done to LOG_LOCALn instead of\n" " LOG_USER, add '.n' to LEVEL, as in 3.7 (n=0..7)\n" " -L DIR, --lib=DIR search for plugins in DIR (default is %s)\n" " --lirc[=PATH] use a LIRC remote control device, attached to PATH\n" " (default: %s)\n" " --localedir=DIR search for locale files in DIR (default is\n" " %s)\n" " -m, --mute mute audio of the primary DVB device at startup\n" " --no-kbd don't use the keyboard as an input device\n" " -p PORT, --port=PORT use PORT for SVDRP (default: %d)\n" " 0 turns off SVDRP\n" " -P OPT, --plugin=OPT load a plugin defined by the given options\n" " -r CMD, --record=CMD call CMD before and after a recording, and after\n" " a recording has been edited or deleted\n" " --resdir=DIR read resource files from DIR (default: %s)\n" " -s CMD, --shutdown=CMD call CMD to shutdown the computer\n" " --split split edited files at the editing marks (only\n" " useful in conjunction with --edit)\n" " --showargs[=DIR] print the arguments read from DIR and exit\n" " (default: %s)\n" " -t TTY, --terminal=TTY controlling tty\n" " -u USER, --user=USER run as user USER; only applicable if started as\n" " root; USER can be a user name or a numerical id\n" " --updindex=REC update index for recording REC and exit\n" " --userdump allow coredumps if -u is given (debugging)\n" " -v DIR, --video=DIR use DIR as video directory (default: %s)\n" " -V, --version print version information and exit\n" " --vfat for backwards compatibility (same as\n" " --dirnames=250,40,1)\n" " -w SEC, --watchdog=SEC activate the watchdog timer with a timeout of SEC\n" " seconds (default: %d); '0' disables the watchdog\n" "\n", DEFAULTCACHEDIR, DEFAULTCONFDIR, PATH_MAX - 1, NAME_MAX, DEFAULTEPGDATAFILENAME, MAXVIDEOFILESIZEDEFAULT, DEFAULTPLUGINDIR, LIRC_DEVICE, DEFAULTLOCDIR, DEFAULTSVDRPPORT, DEFAULTRESDIR, DEFAULTARGSDIR, DEFAULTVIDEODIR, DEFAULTWATCHDOG ); } if (DisplayVersion) printf("vdr (%s/%s) - The Video Disk Recorder\n", VDRVERSION, APIVERSION); if (PluginManager.HasPlugins()) { if (DisplayHelp) printf("Plugins: vdr -P\"name [OPTIONS]\"\n\n"); for (int i = 0; ; i++) { cPlugin *p = PluginManager.GetPlugin(i); if (p) { const char *help = p->CommandLineHelp(); printf("%s (%s) - %s\n", p->Name(), p->Version(), p->Description()); if (DisplayHelp && help) { printf("\n"); puts(help); } } else break; } } return 0; } // Log file: if (SysLogLevel > 0) openlog("vdr", LOG_CONS, SysLogTarget); // LOG_PID doesn't work as expected under NPTL // Check the video directory: if (!DirectoryOk(VideoDirectory, true)) { fprintf(stderr, "vdr: can't access video directory %s\n", VideoDirectory); return 2; } // Daemon mode: if (DaemonMode) { if (daemon(1, 0) == -1) { fprintf(stderr, "vdr: %m\n"); esyslog("ERROR: %m"); return 2; } } else if (Terminal) { // Claim new controlling terminal stdin = freopen(Terminal, "r", stdin); stdout = freopen(Terminal, "w", stdout); stderr = freopen(Terminal, "w", stderr); HasStdin = true; tcgetattr(STDIN_FILENO, &savedTm); } isyslog("VDR version %s started", VDRVERSION); if (StartedAsRoot && VdrUser) isyslog("switched to user '%s'", VdrUser); if (DaemonMode) dsyslog("running as daemon (tid=%d)", cThread::ThreadId()); cThread::SetMainThreadId(); // Set the system character table: char *CodeSet = NULL; if (setlocale(LC_CTYPE, "")) CodeSet = nl_langinfo(CODESET); else { char *LangEnv = getenv("LANG"); // last resort in case locale stuff isn't installed if (LangEnv) { CodeSet = strchr(LangEnv, '.'); if (CodeSet) CodeSet++; // skip the dot } } if (CodeSet) { bool known = SI::SetSystemCharacterTable(CodeSet); isyslog("codeset is '%s' - %s", CodeSet, known ? "known" : "unknown"); cCharSetConv::SetSystemCharacterTable(CodeSet); } if (OverrideCharacterTable) { bool known = SI::SetOverrideCharacterTable(OverrideCharacterTable); isyslog("override character table is '%s' - %s", OverrideCharacterTable, known ? "known" : "unknown"); } // Initialize internationalization: I18nInitialize(LocaleDirectory); // Main program loop variables - need to be here to have them initialized before any EXIT(): cEpgDataReader EpgDataReader; cOsdObject *Menu = NULL; int LastChannel = 0; int LastTimerChannel = -1; int PreviousChannel[2] = { 1, 1 }; int PreviousChannelIndex = 0; time_t LastChannelChanged = time(NULL); time_t LastInteract = 0; int MaxLatencyTime = 0; bool InhibitEpgScan = false; bool IsInfoMenu = false; cSkin *CurrentSkin = NULL; int OldPrimaryDVB = 0; // Load plugins: if (!PluginManager.LoadPlugins(true)) EXIT(2); // Directories: if (!ConfigDirectory) ConfigDirectory = DEFAULTCONFDIR; cPlugin::SetConfigDirectory(ConfigDirectory); if (!CacheDirectory) CacheDirectory = DEFAULTCACHEDIR; cPlugin::SetCacheDirectory(CacheDirectory); if (!ResourceDirectory) ResourceDirectory = DEFAULTRESDIR; cPlugin::SetResourceDirectory(ResourceDirectory); cThemes::SetThemesDirectory(AddDirectory(ConfigDirectory, "themes")); // Configuration data: Setup.Load(AddDirectory(ConfigDirectory, "setup.conf")); Sources.Load(AddDirectory(ConfigDirectory, "sources.conf"), true, true); Diseqcs.Load(AddDirectory(ConfigDirectory, "diseqc.conf"), true, Setup.DiSEqC); Scrs.Load(AddDirectory(ConfigDirectory, "scr.conf"), true); cChannels::Load(AddDirectory(ConfigDirectory, "channels.conf"), false, true); cTimers::Load(AddDirectory(ConfigDirectory, "timers.conf")); Commands.Load(AddDirectory(ConfigDirectory, "commands.conf")); RecordingCommands.Load(AddDirectory(ConfigDirectory, "reccmds.conf")); SVDRPhosts.Load(AddDirectory(ConfigDirectory, "svdrphosts.conf"), true); Keys.Load(AddDirectory(ConfigDirectory, "remote.conf")); KeyMacros.Load(AddDirectory(ConfigDirectory, "keymacros.conf"), true); Folders.Load(AddDirectory(ConfigDirectory, "folders.conf")); CamResponsesLoad(AddDirectory(ConfigDirectory, "camresponses.conf"), true); DoneRecordingsPattern.Load(AddDirectory(CacheDirectory, "donerecs.data")); if (!*cFont::GetFontFileName(Setup.FontOsd)) { const char *msg = "no fonts available - OSD will not show any text!"; fprintf(stderr, "vdr: %s\n", msg); esyslog("ERROR: %s", msg); } // Recordings: cRecordings::Update(); // EPG data: if (EpgDataFileName) { const char *EpgDirectory = NULL; if (DirectoryOk(EpgDataFileName)) { EpgDirectory = EpgDataFileName; EpgDataFileName = DEFAULTEPGDATAFILENAME; } else if (*EpgDataFileName != '/' && *EpgDataFileName != '.') EpgDirectory = CacheDirectory; if (EpgDirectory) cSchedules::SetEpgDataFileName(AddDirectory(EpgDirectory, EpgDataFileName)); else cSchedules::SetEpgDataFileName(EpgDataFileName); EpgDataReader.Start(); } // DVB interfaces: cDvbDevice::Initialize(); cDvbDevice::BondDevices(Setup.DeviceBondings); // Initialize plugins: if (!PluginManager.InitializePlugins()) EXIT(2); // Primary device: cDevice::SetPrimaryDevice(Setup.PrimaryDVB); if (!cDevice::PrimaryDevice() || !cDevice::PrimaryDevice()->HasDecoder()) { if (cDevice::PrimaryDevice() && !cDevice::PrimaryDevice()->HasDecoder()) isyslog("device %d has no MPEG decoder", cDevice::PrimaryDevice()->DeviceNumber() + 1); for (int i = 0; i < cDevice::NumDevices(); i++) { cDevice *d = cDevice::GetDevice(i); if (d && d->HasDecoder()) { isyslog("trying device number %d instead", i + 1); if (cDevice::SetPrimaryDevice(i + 1)) { Setup.PrimaryDVB = i + 1; break; } } } if (!cDevice::PrimaryDevice()) { const char *msg = "no primary device found - using first device!"; fprintf(stderr, "vdr: %s\n", msg); esyslog("ERROR: %s", msg); if (!cDevice::SetPrimaryDevice(1)) EXIT(2); if (!cDevice::PrimaryDevice()) { const char *msg = "no primary device found - giving up!"; fprintf(stderr, "vdr: %s\n", msg); esyslog("ERROR: %s", msg); EXIT(2); } } } OldPrimaryDVB = Setup.PrimaryDVB; // Check for timers in automatic start time window: ShutdownHandler.CheckManualStart(MANUALSTART); // User interface: Interface = new cInterface; // Default skins: new cSkinLCARS; new cSkinSTTNG; new cSkinClassic; Skins.SetCurrent(Setup.OSDSkin); cThemes::Load(Skins.Current()->Name(), Setup.OSDTheme, Skins.Current()->Theme()); CurrentSkin = Skins.Current(); // Start plugins: if (!PluginManager.StartPlugins()) EXIT(2); // Set skin and theme in case they're implemented by a plugin: if (!CurrentSkin || CurrentSkin == Skins.Current() && strcmp(Skins.Current()->Name(), Setup.OSDSkin) != 0) { Skins.SetCurrent(Setup.OSDSkin); cThemes::Load(Skins.Current()->Name(), Setup.OSDTheme, Skins.Current()->Theme()); } // Remote Controls: if (LircDevice) new cLircRemote(LircDevice); if (!DaemonMode && HasStdin && UseKbd) new cKbdRemote; Interface->LearnKeys(); // External audio: if (AudioCommand) new cExternalAudio(AudioCommand); // Positioner: if (!cPositioner::GetPositioner()) // no plugin has created a positioner new cDiseqcPositioner; // CAM data: ChannelCamRelations.Load(AddDirectory(CacheDirectory, "cam.data")); // Channel: if (!cDevice::WaitForAllDevicesReady(DEVICEREADYTIMEOUT)) dsyslog("not all devices ready after %d seconds", DEVICEREADYTIMEOUT); if (!CamSlots.WaitForAllCamSlotsReady(DEVICEREADYTIMEOUT)) dsyslog("not all CAM slots ready after %d seconds", DEVICEREADYTIMEOUT); if (*Setup.InitialChannel) { LOCK_CHANNELS_READ; if (isnumber(Setup.InitialChannel)) { // for compatibility with old setup.conf files if (const cChannel *Channel = Channels->GetByNumber(atoi(Setup.InitialChannel))) Setup.InitialChannel = Channel->GetChannelID().ToString(); } if (const cChannel *Channel = Channels->GetByChannelID(tChannelID::FromString(Setup.InitialChannel))) Setup.CurrentChannel = Channel->Number(); } if (Setup.InitialVolume >= 0) Setup.CurrentVolume = Setup.InitialVolume; { LOCK_CHANNELS_READ; Channels->SwitchTo(Setup.CurrentChannel); } if (MuteAudio) cDevice::PrimaryDevice()->ToggleMute(); else cDevice::PrimaryDevice()->SetVolume(Setup.CurrentVolume, true); // 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); if (signal(SIGPIPE, SignalHandler) == SIG_IGN) signal(SIGPIPE, SIG_IGN); if (WatchdogTimeout > 0) if (signal(SIGALRM, Watchdog) == SIG_IGN) signal(SIGALRM, SIG_IGN); // Watchdog: if (WatchdogTimeout > 0) { dsyslog("setting watchdog timer to %d seconds", WatchdogTimeout); alarm(WatchdogTimeout); // Initial watchdog timer start } #ifdef SDNOTIFY if (sd_watchdog_enabled(0, NULL) > 0) { uint64_t timeout; SdWatchdog = time(NULL); sd_watchdog_enabled(0, &timeout); SdWatchdogTimeout = (int)timeout/1000000; dsyslog("SD_WATCHDOG enabled with timeout set to %d seconds", SdWatchdogTimeout); } // Startup notification: sd_notify(0, "READY=1\nSTATUS=Ready"); #endif // SVDRP: SetSVDRPPorts(SVDRPport, DEFAULTSVDRPPORT); StartSVDRPHandler(); // Main program loop: #define DELETE_MENU ((IsInfoMenu &= (Menu == NULL)), delete Menu, Menu = NULL) while (!ShutdownHandler.DoExit()) { #ifdef DEBUGRINGBUFFERS cRingBufferLinear::PrintDebugRBL(); #endif // Attach launched player control: cControl::Attach(); time_t Now = time(NULL); // Make sure we have a visible programme in case device usage has changed: if (!EITScanner.Active() && cDevice::PrimaryDevice()->HasDecoder()) { static time_t lastTime = 0; if (!cDevice::PrimaryDevice()->HasProgramme()) { if (!CamMenuActive() && Now - lastTime > MINCHANNELWAIT) { // !CamMenuActive() to avoid interfering with the CAM if a CAM menu is open LOCK_CHANNELS_READ; const cChannel *Channel = Channels->GetByNumber(cDevice::CurrentChannel()); if (Channel && (Channel->Vpid() || Channel->Apid(0) || Channel->Dpid(0))) { if (cDevice::GetDeviceForTransponder(Channel, LIVEPRIORITY) && Channels->SwitchTo(Channel->Number())) // try to switch to the original channel... ; else if (LastTimerChannel > 0) { Channel = Channels->GetByNumber(LastTimerChannel); if (Channel && cDevice::GetDeviceForTransponder(Channel, LIVEPRIORITY) && Channels->SwitchTo(LastTimerChannel)) // ...or the one used by the last timer ; } } lastTime = Now; // don't do this too often LastTimerChannel = -1; } } else lastTime = 0; // makes sure we immediately try again next time } // Update the OSD size: { static time_t lastOsdSizeUpdate = 0; if (Now != lastOsdSizeUpdate) { // once per second cOsdProvider::UpdateOsdSize(); static int OsdState = 0; if (cOsdProvider::OsdSizeChanged(OsdState)) { if (cOsdMenu *OsdMenu = dynamic_cast(Menu)) OsdMenu->Display(); } lastOsdSizeUpdate = Now; } } // Restart the Watchdog timer: if (WatchdogTimeout > 0) { int LatencyTime = WatchdogTimeout - alarm(WatchdogTimeout); if (LatencyTime > MaxLatencyTime) { MaxLatencyTime = LatencyTime; dsyslog("max. latency time %d seconds", MaxLatencyTime); } } #ifdef SDNOTIFY // Ping systemd watchdog when half the timeout is elapsed: if (SdWatchdogTimeout && (Now - SdWatchdog) * 2 > SdWatchdogTimeout) { sd_notify(0, "WATCHDOG=1"); SdWatchdog = Now; dsyslog("SD_WATCHDOG ping"); } #endif // Handle channel and timer modifications: { // Channels and timers need to be stored in a consistent manner, // therefore if one of them is changed, we save both. static time_t ChannelSaveTimeout = 0; static cStateKey TimersStateKey(true); static cStateKey ChannelsStateKey(true); static int ChannelsModifiedByUser = 0; const cTimers *Timers = cTimers::GetTimersRead(TimersStateKey); const cChannels *Channels = cChannels::GetChannelsRead(ChannelsStateKey); if (ChannelSaveTimeout != 1) { if (Channels) { if (Channels->ModifiedByUser(ChannelsModifiedByUser)) ChannelSaveTimeout = 1; // triggers an immediate save else if (!ChannelSaveTimeout) ChannelSaveTimeout = Now + CHANNELSAVEDELTA; } if (Timers) ChannelSaveTimeout = 1; // triggers an immediate save } if (ChannelSaveTimeout && Now > ChannelSaveTimeout && !cRecordControls::Active()) ChannelSaveTimeout = 1; // triggers an immediate save if (Timers && Channels) { Channels->Save(); Timers->Save(); ChannelSaveTimeout = 0; } if (Channels) { for (const cChannel *Channel = Channels->First(); Channel; Channel = Channels->Next(Channel)) { if (Channel->Modification(CHANNELMOD_RETUNE)) { cRecordControls::ChannelDataModified(Channel); if (Channel->Number() == cDevice::CurrentChannel() && cDevice::PrimaryDevice()->HasDecoder()) { if (!cDevice::PrimaryDevice()->Replaying() || cDevice::PrimaryDevice()->Transferring()) { if (cDevice::ActualDevice()->ProvidesTransponder(Channel)) { // avoids retune on devices that don't really access the transponder isyslog("retuning due to modification of channel %d (%s)", Channel->Number(), Channel->Name()); Channels->SwitchTo(Channel->Number()); } } } cStatus::MsgChannelChange(Channel); } } } // State keys are removed in reverse order! if (Channels) ChannelsStateKey.Remove(); if (Timers) TimersStateKey.Remove(); if (ChannelSaveTimeout == 1) { // Only one of them was modified, so we reset the state keys to handle them both in the next turn: ChannelsStateKey.Reset(); TimersStateKey.Reset(); } } // Channel display: if (!EITScanner.Active() && cDevice::CurrentChannel() != LastChannel) { if (!Menu) Menu = new cDisplayChannel(cDevice::CurrentChannel(), LastChannel >= 0); LastChannel = cDevice::CurrentChannel(); LastChannelChanged = Now; } if (Now - LastChannelChanged >= Setup.ZapTimeout && LastChannel != PreviousChannel[PreviousChannelIndex]) PreviousChannel[PreviousChannelIndex ^= 1] = LastChannel; { // Timers and Recordings: static cStateKey TimersStateKey; cTimers *Timers = cTimers::GetTimersWrite(TimersStateKey); { LOCK_CHANNELS_READ; // Channels are needed for spawning pattern timers! // Assign events to timers: static cStateKey SchedulesStateKey; if (TimersStateKey.StateChanged()) SchedulesStateKey.Reset(); // we assign events if either the Timers or the Schedules have changed bool TimersModified = false; if (const cSchedules *Schedules = cSchedules::GetSchedulesRead(SchedulesStateKey)) { Timers->SetSyncStateKey(StateKeySVDRPRemoteTimersPoll); // setting events shall not trigger a remote timer poll... if (Timers->AdjustSpawnedTimers()) { // must do this *before* SetEvents()! StateKeySVDRPRemoteTimersPoll.Reset(); // ...but adjusting spawned timers... TimersModified = true; } if (Timers->SetEvents(Schedules)) TimersModified = true; if (Timers->SpawnPatternTimers(Schedules)) { StateKeySVDRPRemoteTimersPoll.Reset(); // ...or spawning new timers must! TimersModified = true; } SchedulesStateKey.Remove(); } TimersStateKey.Remove(TimersModified); // we need to remove the key here, so that syncing StateKeySVDRPRemoteTimersPoll takes effect! } // Must do all following calls with the exact same time! // Process ongoing recordings: Timers = cTimers::GetTimersWrite(TimersStateKey); bool TimersModified = false; if (cRecordControls::Process(Timers, Now)) TimersModified = true; // Start new recordings: if (cTimer *Timer = Timers->GetMatch(Now)) { if (!cRecordControls::Start(Timers, Timer)) Timer->SetPending(true); else LastTimerChannel = Timer->Channel()->Number(); TimersModified = true; } // Make sure timers "see" their channel early enough: static time_t LastTimerCheck = 0; if (Now - LastTimerCheck > TIMERCHECKDELTA) { // don't do this too often InhibitEpgScan = false; for (cTimer *Timer = Timers->First(); Timer; Timer = Timers->Next(Timer)) { if (Timer->Remote() || Timer->IsPatternTimer()) continue; bool InVpsMargin = false; bool NeedsTransponder = false; if (Timer->HasFlags(tfActive) && !Timer->Recording()) { if (Timer->HasFlags(tfVps)) { if (Timer->Matches(Now, true, Setup.VpsMargin)) { InVpsMargin = true; Timer->SetInVpsMargin(InVpsMargin); } else if (Timer->Event()) { InVpsMargin = Timer->Event()->StartTime() <= Now && Now < Timer->Event()->EndTime(); NeedsTransponder = Timer->Event()->StartTime() - Now < VPSLOOKAHEADTIME * 3600 && !Timer->Event()->SeenWithin(VPSUPTODATETIME); } else { LOCK_SCHEDULES_READ; const cSchedule *Schedule = Schedules->GetSchedule(Timer->Channel()); InVpsMargin = !Schedule; // we must make sure we have the schedule NeedsTransponder = Schedule && !Schedule->PresentSeenWithin(VPSUPTODATETIME); } InhibitEpgScan |= InVpsMargin | NeedsTransponder; } else NeedsTransponder = Timer->Matches(Now, true, TIMERLOOKAHEADTIME); } if (NeedsTransponder || InVpsMargin) { // Find a device that provides the required transponder: cDevice *Device = cDevice::GetDeviceForTransponder(Timer->Channel(), MINPRIORITY); if (!Device && InVpsMargin) Device = cDevice::GetDeviceForTransponder(Timer->Channel(), LIVEPRIORITY); // Switch the device to the transponder: if (Device) { bool HadProgramme = cDevice::PrimaryDevice()->HasProgramme(); if (!Device->IsTunedToTransponder(Timer->Channel())) { if (Device == cDevice::ActualDevice() && !Device->IsPrimaryDevice()) cDevice::PrimaryDevice()->StopReplay(); // stop transfer mode dsyslog("switching device %d to channel %d %s (%s)", Device->DeviceNumber() + 1, Timer->Channel()->Number(), *Timer->Channel()->GetChannelID().ToString(), Timer->Channel()->Name()); if (Device->SwitchChannel(Timer->Channel(), false)) Device->SetOccupied(TIMERDEVICETIMEOUT); } if (cDevice::PrimaryDevice()->HasDecoder() && HadProgramme && !cDevice::PrimaryDevice()->HasProgramme()) Skins.QueueMessage(mtInfo, tr("Upcoming recording!")); // the previous SwitchChannel() has switched away the current live channel } } } LastTimerCheck = Now; } // Delete expired timers: if (Timers->DeleteExpired(TimersModified)) TimersModified = true; // Make sure there is enough free disk space for ongoing recordings: int MaxPriority = Timers->GetMaxPriority(); if (MaxPriority >= 0) AssertFreeDiskSpace(MaxPriority); TimersStateKey.Remove(TimersModified); } // Recordings: if (!Menu) { if (cRecordings::NeedsUpdate()) cRecordings::Update(); } // CAM control: if (!Menu && !cOsd::IsOpen()) Menu = CamControl(); // Queued messages: Skins.ProcessQueuedMessages(); // User Input: bool NeedsFastResponse = Menu && Menu->NeedsFastResponse(); if (!NeedsFastResponse) { // Must limit the scope of ControlMutexLock here to not hold the lock during the call to Interface->GetKey(). cMutexLock ControlMutexLock; cControl *Control = cControl::Control(ControlMutexLock); NeedsFastResponse = Control && Control->NeedsFastResponse(); } eKeys key = Interface->GetKey(!NeedsFastResponse); cOsdObject *Interact = Menu; cMutexLock ControlMutexLock; cControl *Control = NULL; if (!Menu) Interact = Control = cControl::Control(ControlMutexLock); if (ISREALKEY(key)) { EITScanner.Activity(); // Cancel shutdown countdown: if (ShutdownHandler.countdown) ShutdownHandler.countdown.Cancel(); // Set user active for MinUserInactivity time in the future: ShutdownHandler.SetUserInactiveTimeout(); } // Keys that must work independent of any interactive mode: switch (int(key)) { // Menu control: case kMenu: { key = kNone; // nobody else needs to see this key bool WasOpen = Interact != NULL; bool WasMenu = Interact && Interact->IsMenu(); if (Menu) DELETE_MENU; else if (Control) { if (cOsd::IsOpen()) Control->Hide(); else WasOpen = false; } if (!WasOpen || !WasMenu && !Setup.MenuKeyCloses) Menu = new cMenuMain; } break; // Info: case kInfo: { if (IsInfoMenu) { key = kNone; // nobody else needs to see this key DELETE_MENU; } else if (!Menu) { IsInfoMenu = true; if (Control) { Control->Hide(); Menu = Control->GetInfo(); if (Menu) Menu->Show(); else IsInfoMenu = false; } else { cRemote::Put(kOk, true); cRemote::Put(kSchedule, true); } key = kNone; // nobody else needs to see this key } } break; // Direct main menu functions: #define DirectMainFunction(function)\ { DELETE_MENU;\ if (Control)\ Control->Hide();\ Menu = new cMenuMain(function);\ key = kNone; } // nobody else needs to see this key case kSchedule: DirectMainFunction(osSchedule); break; case kChannels: DirectMainFunction(osChannels); break; case kTimers: DirectMainFunction(osTimers); break; case kRecordings: DirectMainFunction(osRecordings); break; case kSetup: DirectMainFunction(osSetup); break; case kCommands: DirectMainFunction(osCommands); break; case kUser0 ... kUser9: cRemote::PutMacro(key); key = kNone; break; case k_Plugin: { const char *PluginName = cRemote::GetPlugin(); if (PluginName) { DELETE_MENU; if (Control) Control->Hide(); cPlugin *plugin = cPluginManager::GetPlugin(PluginName); if (plugin) { Menu = plugin->MainMenuAction(); if (Menu) Menu->Show(); } else esyslog("ERROR: unknown plugin '%s'", PluginName); } key = kNone; // nobody else needs to see these keys } break; // Channel up/down: case kChanUp|k_Repeat: case kChanUp: case kChanDn|k_Repeat: case kChanDn: if (!Interact) { Menu = new cDisplayChannel(NORMALKEY(key)); continue; } else if (cDisplayChannel::IsOpen() || Control) { Interact->ProcessKey(key); continue; } else cDevice::SwitchChannel(NORMALKEY(key) == kChanUp ? 1 : -1); break; // Volume control: case kVolUp|k_Repeat: case kVolUp: case kVolDn|k_Repeat: case kVolDn: case kMute: if (key == kMute) { if (!cDevice::PrimaryDevice()->ToggleMute() && !Menu) { key = kNone; // nobody else needs to see these keys break; // no need to display "mute off" } } else cDevice::PrimaryDevice()->SetVolume(NORMALKEY(key) == kVolDn ? -VOLUMEDELTA : VOLUMEDELTA); if (!Menu && !cOsd::IsOpen()) Menu = cDisplayVolume::Create(); cDisplayVolume::Process(key); key = kNone; // nobody else needs to see these keys break; // Audio track control: case kAudio: if (Control) Control->Hide(); if (!cDisplayTracks::IsOpen()) { DELETE_MENU; Menu = cDisplayTracks::Create(); } else cDisplayTracks::Process(key); key = kNone; break; // Subtitle track control: case kSubtitles: if (Control) Control->Hide(); if (!cDisplaySubtitleTracks::IsOpen()) { DELETE_MENU; Menu = cDisplaySubtitleTracks::Create(); } else cDisplaySubtitleTracks::Process(key); key = kNone; break; // Pausing live video: case kPlayPause: case kPause: if (!Control) { DELETE_MENU; if (Setup.PauseKeyHandling) { if (Setup.PauseKeyHandling > 1 || Interface->Confirm(tr("Pause live video?"))) { if (!cRecordControls::PauseLiveVideo()) Skins.QueueMessage(mtError, tr("No free DVB device to record!")); } } key = kNone; // nobody else needs to see this key } break; // Instant recording: case kRecord: if (!Control) { if (Setup.RecordKeyHandling) { if (Setup.RecordKeyHandling > 1 || Interface->Confirm(tr("Start recording?"))) { if (cRecordControls::Start()) Skins.QueueMessage(mtInfo, tr("Recording started")); } } key = kNone; // nobody else needs to see this key } break; // Power off: case kPower: isyslog("Power button pressed"); DELETE_MENU; // Check for activity, request power button again if active: if (!ShutdownHandler.ConfirmShutdown(false) && Skins.Message(mtWarning, tr("VDR will shut down later - press Power to force"), SHUTDOWNFORCEPROMPT) != kPower) { // Not pressed power - set VDR to be non-interactive and power down later: ShutdownHandler.SetUserInactive(); break; } // No activity or power button pressed twice - ask for confirmation: if (!ShutdownHandler.ConfirmShutdown(true)) { // Non-confirmed background activity - set VDR to be non-interactive and power down later: ShutdownHandler.SetUserInactive(); break; } // Ask the final question: if (!Interface->Confirm(tr("Press any key to cancel shutdown"), SHUTDOWNCANCELPROMPT, true)) // If final question was canceled, continue to be active: break; // Ok, now call the shutdown script: ShutdownHandler.DoShutdown(true); // Set VDR to be non-interactive and power down again later: ShutdownHandler.SetUserInactive(); // Do not attempt to automatically shut down for a while: ShutdownHandler.SetRetry(SHUTDOWNRETRY); break; default: break; } Interact = Menu ? Menu : Control; // might have been closed in the mean time if (Interact) { LastInteract = Now; eOSState state = Interact->ProcessKey(key); if (state == osUnknown && Interact != Control) { if (ISMODELESSKEY(key) && Control) { state = Control->ProcessKey(key); if (state == osEnd) { // let's not close a menu when replay ends: Control = NULL; cControl::Shutdown(); continue; } } else if (Now - cRemote::LastActivity() > MENUTIMEOUT) state = osEnd; } switch (state) { case osPause: DELETE_MENU; if (!cRecordControls::PauseLiveVideo()) Skins.QueueMessage(mtError, tr("No free DVB device to record!")); break; case osRecord: DELETE_MENU; if (cRecordControls::Start()) Skins.QueueMessage(mtInfo, tr("Recording started")); break; case osRecordings: DELETE_MENU; Control = NULL; cControl::Shutdown(); Menu = new cMenuMain(osRecordings, true); break; case osReplay: DELETE_MENU; Control = NULL; cControl::Shutdown(); cControl::Launch(new cReplayControl); break; case osStopReplay: DELETE_MENU; Control = NULL; cControl::Shutdown(); break; case osPlugin: DELETE_MENU; Menu = cMenuMain::PluginOsdObject(); if (Menu) Menu->Show(); break; case osBack: case osEnd: if (Interact == Menu) DELETE_MENU; else { Control = NULL; cControl::Shutdown(); } break; default: ; } } else { // Key functions in "normal" viewing mode: if (key != kNone && KeyMacros.Get(key)) { cRemote::PutMacro(key); key = kNone; } switch (int(key)) { // Toggle channels: case kChanPrev: case k0: { if (PreviousChannel[PreviousChannelIndex ^ 1] == LastChannel || LastChannel != PreviousChannel[0] && LastChannel != PreviousChannel[1]) PreviousChannelIndex ^= 1; LOCK_CHANNELS_READ; Channels->SwitchTo(PreviousChannel[PreviousChannelIndex ^= 1]); break; } // Direct Channel Select: case k1 ... k9: // Left/Right rotates through channel groups: case kLeft|k_Repeat: case kLeft: case kRight|k_Repeat: case kRight: // Previous/Next rotates through channel groups: case kPrev|k_Repeat: case kPrev: case kNext|k_Repeat: case kNext: // Up/Down Channel Select: case kUp|k_Repeat: case kUp: case kDown|k_Repeat: case kDown: Menu = new cDisplayChannel(NORMALKEY(key)); break; // Viewing Control: case kOk: LastChannel = -1; break; // forces channel display // Instant resume of the last viewed recording: case kPlay: if (cReplayControl::LastReplayed()) { Control = NULL; cControl::Shutdown(); cControl::Launch(new cReplayControl); } else DirectMainFunction(osRecordings); // no last viewed recording, so enter the Recordings menu break; default: break; } } if (!Menu) { if (!InhibitEpgScan) EITScanner.Process(); bool Error = false; if (RecordingsHandler.Finished(Error)) { if (Error) Skins.Message(mtError, tr("Editing process failed!")); else Skins.Message(mtInfo, tr("Editing process finished")); } } // Change primary device: int NewPrimaryDVB = Setup.PrimaryDVB; if (NewPrimaryDVB != OldPrimaryDVB) { DELETE_MENU; Control = NULL; cControl::Shutdown(); Skins.QueueMessage(mtInfo, tr("Switching primary DVB...")); cOsdProvider::Shutdown(); cDevice::SetPrimaryDevice(NewPrimaryDVB); OldPrimaryDVB = NewPrimaryDVB; } // SIGHUP shall cause a restart: if (LastSignal == SIGHUP) { if (ShutdownHandler.ConfirmRestart(true) && Interface->Confirm(tr("Press any key to cancel restart"), RESTARTCANCELPROMPT, true)) EXIT(1); LastSignal = 0; } // Update the shutdown countdown: if (ShutdownHandler.countdown && ShutdownHandler.countdown.Update()) { if (!ShutdownHandler.ConfirmShutdown(false)) ShutdownHandler.countdown.Cancel(); } if (!Control && !cRecordControls::Active() && !RecordingsHandler.Active() && (Now - cRemote::LastActivity()) > ACTIVITYTIMEOUT) { // Shutdown: // Check whether VDR will be ready for shutdown in SHUTDOWNWAIT seconds: time_t Soon = Now + SHUTDOWNWAIT; if (ShutdownHandler.IsUserInactive(Soon) && ShutdownHandler.Retry(Soon) && !ShutdownHandler.countdown) { if (ShutdownHandler.ConfirmShutdown(false)) // Time to shut down - start final countdown: ShutdownHandler.countdown.Start(tr("VDR will shut down in %s minutes"), SHUTDOWNWAIT); // the placeholder is really %s! // Dont try to shut down again for a while: ShutdownHandler.SetRetry(SHUTDOWNRETRY); } // Countdown run down to 0? if (ShutdownHandler.countdown.Done()) { // Timed out, now do a final check: if (ShutdownHandler.IsUserInactive() && ShutdownHandler.ConfirmShutdown(false)) ShutdownHandler.DoShutdown(false); // Do this again a bit later: ShutdownHandler.SetRetry(SHUTDOWNRETRY); } // Handle housekeeping tasks if ((Now - LastInteract) > ACTIVITYTIMEOUT) { // Disk housekeeping: RemoveDeletedRecordings(); ListGarbageCollector.Purge(); cSchedules::Cleanup(); // Plugins housekeeping: PluginManager.Housekeeping(); } } ReportEpgBugFixStats(); // Main thread hooks of plugins: PluginManager.MainThreadHook(); } if (ShutdownHandler.EmergencyExitRequested()) esyslog("emergency exit requested - shutting down"); Exit: // Reset all signal handlers to default before Interface gets deleted: signal(SIGHUP, SIG_DFL); signal(SIGINT, SIG_DFL); signal(SIGTERM, SIG_DFL); signal(SIGPIPE, SIG_DFL); signal(SIGALRM, SIG_DFL); StopSVDRPHandler(); ChannelCamRelations.Save(); cRecordControls::Shutdown(); PluginManager.StopPlugins(); RecordingsHandler.DelAll(); delete Menu; cControl::Shutdown(); delete Interface; cOsdProvider::Shutdown(); Remotes.Clear(); Audios.Clear(); Skins.Clear(); SourceParams.Clear(); if (ShutdownHandler.GetExitCode() != 2) { Setup.CurrentChannel = cDevice::CurrentChannel(); Setup.CurrentVolume = cDevice::CurrentVolume(); Setup.Save(); } cDevice::Shutdown(); cPositioner::DestroyPositioner(); cVideoDirectory::Destroy(); EpgHandlers.Clear(); cSchedules::Cleanup(true); CiResourceHandlers.Clear(); ListGarbageCollector.Purge(true); PluginManager.Shutdown(true); ReportEpgBugFixStats(true); if (WatchdogTimeout > 0) dsyslog("max. latency time %d seconds", MaxLatencyTime); if (LastSignal) isyslog("caught signal %d", LastSignal); if (ShutdownHandler.EmergencyExitRequested()) esyslog("emergency exit!"); isyslog("exiting, exit code %d", ShutdownHandler.GetExitCode()); if (SysLogLevel > 0) closelog(); if (HasStdin) tcsetattr(STDIN_FILENO, TCSANOW, &savedTm); #ifdef SDNOTIFY if (ShutdownHandler.GetExitCode() == 2) sd_notify(0, "STOPPING=1\nSTATUS=Startup failed, exiting"); else sd_notify(0, "STOPPING=1\nSTATUS=Exiting"); #endif return ShutdownHandler.GetExitCode(); }