2 * Copyright (C) 2013 Team XBMC
5 * This Program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
10 * This Program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with XBMC; see the file COPYING. If not, see
17 * <http://www.gnu.org/licenses/>.
22 #include <netinet/in.h>
23 #include <sys/socket.h>
24 #include <arpa/inet.h>
27 #include "network/Network.h"
28 #include "Application.h"
29 #include "DNSNameCache.h"
30 #include "dialogs/GUIDialogProgress.h"
31 #include "dialogs/GUIDialogKaiToast.h"
32 #include "filesystem/SpecialProtocol.h"
33 #include "guilib/GUIWindowManager.h"
34 #include "guilib/LocalizeStrings.h"
35 #include "settings/AdvancedSettings.h"
36 #include "settings/Settings.h"
37 #include "settings/MediaSourceSettings.h"
38 #include "utils/JobManager.h"
39 #include "utils/log.h"
40 #include "utils/XMLUtils.h"
41 #include "utils/URIUtils.h"
42 #include "utils/StringUtils.h"
44 #include "WakeOnAccess.h"
48 #define DEFAULT_NETWORK_INIT_SEC (20) // wait 20 sec for network after startup or resume
49 #define DEFAULT_NETWORK_SETTLE_MS (500) // require 500ms of consistent network availability before trusting it
51 #define DEFAULT_TIMEOUT_SEC (5*60) // at least 5 minutes between each magic packets
52 #define DEFAULT_WAIT_FOR_ONLINE_SEC_1 (40) // wait at 40 seconds after sending magic packet
53 #define DEFAULT_WAIT_FOR_ONLINE_SEC_2 (40) // same for extended wait
54 #define DEFAULT_WAIT_FOR_SERVICES_SEC (5) // wait 5 seconds after host go online to launch file sharing deamons
56 static int GetTotalSeconds(const CDateTimeSpan& ts)
58 int hours = ts.GetHours() + ts.GetDays() * 24;
59 int minutes = ts.GetMinutes() + hours * 60;
60 return ts.GetSeconds() + minutes * 60;
63 static unsigned long HostToIP(const CStdString& host)
66 CDNSNameCache::Lookup(host, ip);
67 return inet_addr(ip.c_str());
70 CWakeOnAccess::WakeUpEntry::WakeUpEntry (bool isAwake)
71 : timeout (0, 0, 0, DEFAULT_TIMEOUT_SEC)
72 , wait_online1_sec(DEFAULT_WAIT_FOR_ONLINE_SEC_1)
73 , wait_online2_sec(DEFAULT_WAIT_FOR_ONLINE_SEC_2)
74 , wait_services_sec(DEFAULT_WAIT_FOR_SERVICES_SEC)
75 , ping_port(0), ping_mode(0)
77 nextWake = CDateTime::GetCurrentDateTime();
85 class CMACDiscoveryJob : public CJob
88 CMACDiscoveryJob(const CStdString& host) : m_host(host) {}
90 virtual bool DoWork();
92 const CStdString& GetMAC() const { return m_macAddres; }
93 const CStdString& GetHost() const { return m_host; }
96 CStdString m_macAddres;
100 bool CMACDiscoveryJob::DoWork()
102 unsigned long ipAddress = HostToIP(m_host);
104 if (ipAddress == INADDR_NONE)
106 CLog::Log(LOGERROR, "%s - can't determine ip of '%s'", __FUNCTION__, m_host.c_str());
110 vector<CNetworkInterface*>& ifaces = g_application.getNetwork().GetInterfaceList();
111 for (vector<CNetworkInterface*>::const_iterator it = ifaces.begin(); it != ifaces.end(); ++it)
113 if ((*it)->GetHostMacAddress(ipAddress, m_macAddres))
125 virtual bool SuccessWaiting () const { return false; }
133 NestDetect() : m_gui_thread (g_application.IsCurrentThread())
147 bool IsNested() const
149 return m_gui_thread && m_nest > 1;
154 const bool m_gui_thread;
156 int NestDetect::m_nest = 0;
160 class ProgressDialogHelper
163 ProgressDialogHelper (const CStdString& heading) : m_dialog(0)
165 if (g_application.IsCurrentThread())
166 m_dialog = (CGUIDialogProgress*) g_windowManager.GetWindow(WINDOW_DIALOG_PROGRESS);
170 m_dialog->SetHeading (heading);
171 m_dialog->SetLine(0, "");
172 m_dialog->SetLine(1, "");
173 m_dialog->SetLine(2, "");
175 int nest_level = NestDetect::Level();
178 CStdString nest = StringUtils::Format("Nesting:%d", nest_level);
179 m_dialog->SetLine(2, nest);
183 ~ProgressDialogHelper ()
189 bool HasDialog() const { return m_dialog != 0; }
191 enum wait_result { TimedOut, Canceled, Success };
193 wait_result ShowAndWait (const WaitCondition& waitObj, unsigned timeOutSec, const CStdString& line1)
195 unsigned timeOutMs = timeOutSec * 1000;
199 m_dialog->SetLine(1, line1);
201 m_dialog->SetPercentage(1); // avoid flickering by starting at 1% ..
204 XbmcThreads::EndTime end_time (timeOutMs);
206 while (!end_time.IsTimePast())
208 if (waitObj.SuccessWaiting())
213 if (!m_dialog->IsActive())
214 m_dialog->StartModal();
216 if (m_dialog->IsCanceled())
219 m_dialog->Progress();
221 unsigned ms_passed = timeOutMs - end_time.MillisLeft();
223 int percentage = (ms_passed * 100) / timeOutMs;
224 m_dialog->SetPercentage(max(percentage, 1)); // avoid flickering , keep minimum 1%
227 Sleep (m_dialog ? 20 : 200);
234 CGUIDialogProgress* m_dialog;
237 class NetworkStartWaiter : public WaitCondition
240 NetworkStartWaiter (unsigned settle_time_ms, const CStdString& host) : m_settle_time_ms (settle_time_ms), m_host(host)
243 virtual bool SuccessWaiting () const
245 unsigned long address = ntohl(HostToIP(m_host));
246 bool online = g_application.getNetwork().HasInterfaceForIP(address);
248 if (!online) // setup endtime so we dont return true until network is consistently connected
249 m_end.Set (m_settle_time_ms);
251 return online && m_end.IsTimePast();
254 mutable XbmcThreads::EndTime m_end;
255 unsigned m_settle_time_ms;
256 const CStdString m_host;
259 class PingResponseWaiter : public WaitCondition, private IJobCallback
262 PingResponseWaiter (bool async, const CWakeOnAccess::WakeUpEntry& server)
263 : m_server(server), m_jobId(0), m_hostOnline(false)
267 CJob* job = new CHostProberJob(server);
268 m_jobId = CJobManager::GetInstance().AddJob(job, this);
271 ~PingResponseWaiter()
273 CJobManager::GetInstance().CancelJob(m_jobId);
275 virtual bool SuccessWaiting () const
277 return m_jobId ? m_hostOnline : Ping(m_server);
280 virtual void OnJobComplete(unsigned int jobID, bool success, CJob *job)
282 m_hostOnline = success;
285 static bool Ping (const CWakeOnAccess::WakeUpEntry& server)
287 ULONG dst_ip = HostToIP(server.host);
289 return g_application.getNetwork().PingHost(dst_ip, server.ping_port, 2000, server.ping_mode & 1);
293 class CHostProberJob : public CJob
296 CHostProberJob(const CWakeOnAccess::WakeUpEntry& server) : m_server (server) {}
298 virtual bool DoWork()
300 while (!ShouldCancel(0,0))
302 if (PingResponseWaiter::Ping(m_server))
309 const CWakeOnAccess::WakeUpEntry& m_server;
312 const CWakeOnAccess::WakeUpEntry& m_server;
313 unsigned int m_jobId;
319 CWakeOnAccess::CWakeOnAccess()
320 : m_netinit_sec(DEFAULT_NETWORK_INIT_SEC) // wait for network to connect
321 , m_netsettle_ms(DEFAULT_NETWORK_SETTLE_MS) // wait for network to settle
326 CWakeOnAccess &CWakeOnAccess::Get()
328 static CWakeOnAccess sWakeOnAccess;
329 return sWakeOnAccess;
332 bool CWakeOnAccess::WakeUpHost(const CURL& url)
334 CStdString hostName = url.GetHostName();
336 if (!hostName.empty())
337 return WakeUpHost (hostName, url.Get());
341 bool CWakeOnAccess::WakeUpHost (const CStdString& hostName, const string& customMessage)
344 return true; // bail if feature is turned off
348 if (FindOrTouchHostEntry(hostName, server))
350 CLog::Log(LOGNOTICE,"WakeOnAccess [%s] trigged by accessing : %s", hostName.c_str(), customMessage.c_str());
352 NestDetect nesting ; // detect recursive calls on gui thread..
354 if (nesting.IsNested()) // we might get in trouble if it gets called back in loop
355 CLog::Log(LOGWARNING,"WakeOnAccess recursively called on gui-thread [%d]", NestDetect::Level());
357 bool ret = WakeUpHost(server);
359 if (!ret) // extra log if we fail for some reason
360 CLog::Log(LOGWARNING,"WakeOnAccess failed to bring up [%s] - there may be trouble ahead !", hostName.c_str());
362 TouchHostEntry(hostName);
369 #define LOCALIZED(id) g_localizeStrings.Get(id)
371 bool CWakeOnAccess::WakeUpHost(const WakeUpEntry& server)
373 CStdString heading = StringUtils::Format(LOCALIZED(13027), server.host.c_str());
375 ProgressDialogHelper dlg (heading);
378 NetworkStartWaiter waitObj (m_netsettle_ms, server.host); // wait until network connected before sending wake-on-lan
380 if (dlg.ShowAndWait (waitObj, m_netinit_sec, LOCALIZED(13028)) != ProgressDialogHelper::Success)
382 CLog::Log(LOGNOTICE,"WakeOnAccess timeout/cancel while waiting for network");
383 return false; // timedout or canceled
388 ULONG dst_ip = HostToIP(server.host);
390 if (g_application.getNetwork().PingHost(dst_ip, server.ping_port, 500)) // quick ping with short timeout to not block too long
392 CLog::Log(LOGNOTICE,"WakeOnAccess success exit, server already running");
397 if (!g_application.getNetwork().WakeOnLan(server.mac.c_str()))
399 CLog::Log(LOGERROR,"WakeOnAccess failed to send. (Is it blocked by firewall?)");
401 if (g_application.IsCurrentThread() || !g_application.m_pPlayer->IsPlaying())
402 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, heading, LOCALIZED(13029));
407 PingResponseWaiter waitObj (dlg.HasDialog(), server); // wait for ping response ..
409 ProgressDialogHelper::wait_result
410 result = dlg.ShowAndWait (waitObj, server.wait_online1_sec, LOCALIZED(13030));
412 if (result == ProgressDialogHelper::TimedOut)
413 result = dlg.ShowAndWait (waitObj, server.wait_online2_sec, LOCALIZED(13031));
415 if (result != ProgressDialogHelper::Success)
417 CLog::Log(LOGNOTICE,"WakeOnAccess timeout/cancel while waiting for response");
418 return false; // timedout or canceled
422 // we have ping response ; just add extra wait-for-services before returning if requested
425 WaitCondition waitObj ; // wait uninteruptable fixed time for services ..
427 dlg.ShowAndWait (waitObj, server.wait_services_sec, LOCALIZED(13032));
429 CLog::Log(LOGNOTICE,"WakeOnAccess sequence completed, server started");
434 bool CWakeOnAccess::FindOrTouchHostEntry (const CStdString& hostName, WakeUpEntry& result)
436 CSingleLock lock (m_entrylist_protect);
438 bool need_wakeup = false;
440 for (EntriesVector::iterator i = m_entries.begin();i != m_entries.end(); ++i)
442 WakeUpEntry& server = *i;
444 if (hostName.Equals(server.host.c_str()))
446 CDateTime now = CDateTime::GetCurrentDateTime();
448 if (now > server.nextWake)
453 else // 'touch' next wakeup time
455 server.nextWake = now + server.timeout;
465 void CWakeOnAccess::TouchHostEntry (const CStdString& hostName)
467 CSingleLock lock (m_entrylist_protect);
469 for (EntriesVector::iterator i = m_entries.begin();i != m_entries.end(); ++i)
471 WakeUpEntry& server = *i;
473 if (hostName.Equals(server.host.c_str()))
475 server.nextWake = CDateTime::GetCurrentDateTime() + server.timeout;
481 static void AddHost (const CStdString& host, vector<string>& hosts)
483 for (vector<string>::const_iterator it = hosts.begin(); it != hosts.end(); ++it)
484 if (host.Equals((*it).c_str()))
485 return; // allready there ..
488 hosts.push_back(host);
491 static void AddHostFromDatabase(const DatabaseSettings& setting, vector<string>& hosts)
493 if (setting.type.Equals("mysql"))
494 AddHost(setting.host, hosts);
497 void CWakeOnAccess::QueueMACDiscoveryForHost(const CStdString& host)
501 if (URIUtils::IsHostOnLAN(host, true))
502 CJobManager::GetInstance().AddJob(new CMACDiscoveryJob(host), this);
504 CLog::Log(LOGNOTICE, "%s - skip Mac discovery for non-local host '%s'", __FUNCTION__, host.c_str());
508 static void AddHostsFromMediaSource(const CMediaSource& source, std::vector<std::string>& hosts)
510 for (CStdStringArray::const_iterator it = source.vecPaths.begin() ; it != source.vecPaths.end(); it++)
514 AddHost (url.GetHostName(), hosts);
518 static void AddHostsFromVecSource(const VECSOURCES& sources, vector<string>& hosts)
520 for (VECSOURCES::const_iterator it = sources.begin(); it != sources.end(); it++)
521 AddHostsFromMediaSource(*it, hosts);
524 static void AddHostsFromVecSource(const VECSOURCES* sources, vector<string>& hosts)
527 AddHostsFromVecSource(*sources, hosts);
530 void CWakeOnAccess::QueueMACDiscoveryForAllRemotes()
532 vector<string> hosts;
535 CMediaSourceSettings& ms = CMediaSourceSettings::Get();
537 AddHostsFromVecSource(ms.GetSources("video"), hosts);
538 AddHostsFromVecSource(ms.GetSources("music"), hosts);
539 AddHostsFromVecSource(ms.GetSources("files"), hosts);
540 AddHostsFromVecSource(ms.GetSources("pictures"), hosts);
541 AddHostsFromVecSource(ms.GetSources("programs"), hosts);
544 AddHostFromDatabase(g_advancedSettings.m_databaseVideo, hosts);
545 AddHostFromDatabase(g_advancedSettings.m_databaseMusic, hosts);
546 AddHostFromDatabase(g_advancedSettings.m_databaseEpg, hosts);
547 AddHostFromDatabase(g_advancedSettings.m_databaseTV, hosts);
549 // add from path substitutions ..
550 for (CAdvancedSettings::StringMapping::iterator i = g_advancedSettings.m_pathSubstitutions.begin(); i != g_advancedSettings.m_pathSubstitutions.end(); ++i)
552 CURL url = i->second;
554 AddHost (url.GetHostName(), hosts);
557 for (vector<string>::const_iterator it = hosts.begin(); it != hosts.end(); it++)
558 QueueMACDiscoveryForHost(*it);
561 void CWakeOnAccess::SaveMACDiscoveryResult(const CStdString& host, const CStdString& mac)
563 CLog::Log(LOGNOTICE, "%s - Mac discovered for host '%s' -> '%s'", __FUNCTION__, host.c_str(), mac.c_str());
565 CStdString heading = LOCALIZED(13033);
567 for (EntriesVector::iterator i = m_entries.begin(); i != m_entries.end(); ++i)
569 if (host.Equals(i->host.c_str()))
571 CLog::Log(LOGDEBUG, "%s - Update existing entry for host '%s'", __FUNCTION__, host.c_str());
572 if (!mac.Equals(i->mac.c_str()))
574 if (IsEnabled()) // show notification only if we have general feature enabled
576 CStdString message = StringUtils::Format(LOCALIZED(13034), host.c_str());
577 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, heading, message, 4000, true, 3000);
588 // not found entry to update - create using default values
589 WakeUpEntry entry (true);
592 m_entries.push_back(entry);
594 CLog::Log(LOGDEBUG, "%s - Create new entry for host '%s'", __FUNCTION__, host.c_str());
595 if (IsEnabled()) // show notification only if we have general feature enabled
597 CStdString message = StringUtils::Format(LOCALIZED(13035), host.c_str());
598 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Info, heading, message, 4000, true, 3000);
604 void CWakeOnAccess::OnJobComplete(unsigned int jobID, bool success, CJob *job)
606 CMACDiscoveryJob* discoverJob = (CMACDiscoveryJob*)job;
608 const CStdString& host = discoverJob->GetHost();
609 const CStdString& mac = discoverJob->GetMAC();
613 CSingleLock lock (m_entrylist_protect);
615 SaveMACDiscoveryResult(host, mac);
619 CLog::Log(LOGERROR, "%s - Mac discovery failed for host '%s'", __FUNCTION__, host.c_str());
623 CStdString heading = LOCALIZED(13033);
624 CStdString message = StringUtils::Format(LOCALIZED(13036), host.c_str());
625 CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, heading, message, 4000, true, 3000);
630 CStdString CWakeOnAccess::GetSettingFile()
632 return CSpecialProtocol::TranslatePath("special://masterprofile/wakeonlan.xml");
635 void CWakeOnAccess::OnSettingsLoaded()
637 CSingleLock lock (m_entrylist_protect);
642 void CWakeOnAccess::OnSettingsSaved()
644 bool enabled = CSettings::Get().GetBool("powermanagement.wakeonaccess");
646 if (enabled != IsEnabled())
651 QueueMACDiscoveryForAllRemotes();
655 void CWakeOnAccess::LoadFromXML()
657 bool enabled = CSettings::Get().GetBool("powermanagement.wakeonaccess");
661 if (!xmlDoc.LoadFile(GetSettingFile()))
663 CLog::Log(LOGNOTICE, "%s - unable to load:%s", __FUNCTION__, GetSettingFile().c_str());
667 TiXmlElement* pRootElement = xmlDoc.RootElement();
668 if (strcmpi(pRootElement->Value(), "onaccesswakeup"))
670 CLog::Log(LOGERROR, "%s - XML file %s doesnt contain <onaccesswakeup>", __FUNCTION__, GetSettingFile().c_str());
676 CLog::Log(LOGNOTICE,"WakeOnAccess - Load settings :");
679 if (XMLUtils::GetInt(pRootElement, "netinittimeout", tmp, 0, 5 * 60))
681 CLog::Log(LOGNOTICE," -Network init timeout : [%d] sec", m_netinit_sec);
683 if (XMLUtils::GetInt(pRootElement, "netsettletime", tmp, 0, 5 * 1000))
684 m_netsettle_ms = tmp;
685 CLog::Log(LOGNOTICE," -Network settle time : [%d] ms", m_netsettle_ms);
687 const TiXmlNode* pWakeUp = pRootElement->FirstChildElement("wakeup");
693 if (XMLUtils::GetString(pWakeUp, "host", strtmp))
696 if (XMLUtils::GetString(pWakeUp, "mac", strtmp))
699 if (entry.host.empty())
700 CLog::Log(LOGERROR, "%s - Missing <host> tag or it's empty", __FUNCTION__);
701 else if (entry.mac.empty())
702 CLog::Log(LOGERROR, "%s - Missing <mac> tag or it's empty", __FUNCTION__);
705 if (XMLUtils::GetInt(pWakeUp, "pingport", tmp, 0, USHRT_MAX))
706 entry.ping_port = (unsigned short) tmp;
708 if (XMLUtils::GetInt(pWakeUp, "pingmode", tmp, 0, USHRT_MAX))
709 entry.ping_mode = (unsigned short) tmp;
711 if (XMLUtils::GetInt(pWakeUp, "timeout", tmp, 10, 12 * 60 * 60))
712 entry.timeout.SetDateTimeSpan (0, 0, 0, tmp);
714 if (XMLUtils::GetInt(pWakeUp, "waitonline", tmp, 0, 10 * 60)) // max 10 minutes
715 entry.wait_online1_sec = tmp;
717 if (XMLUtils::GetInt(pWakeUp, "waitonline2", tmp, 0, 10 * 60)) // max 10 minutes
718 entry.wait_online2_sec = tmp;
720 if (XMLUtils::GetInt(pWakeUp, "waitservices", tmp, 0, 5 * 60)) // max 5 minutes
721 entry.wait_services_sec = tmp;
723 CLog::Log(LOGNOTICE," Registering wakeup entry:");
724 CLog::Log(LOGNOTICE," HostName : %s", entry.host.c_str());
725 CLog::Log(LOGNOTICE," MacAddress : %s", entry.mac.c_str());
726 CLog::Log(LOGNOTICE," PingPort : %d", entry.ping_port);
727 CLog::Log(LOGNOTICE," PingMode : %d", entry.ping_mode);
728 CLog::Log(LOGNOTICE," Timeout : %d (sec)", GetTotalSeconds(entry.timeout));
729 CLog::Log(LOGNOTICE," WaitForOnline : %d (sec)", entry.wait_online1_sec);
730 CLog::Log(LOGNOTICE," WaitForOnlineEx : %d (sec)", entry.wait_online2_sec);
731 CLog::Log(LOGNOTICE," WaitForServices : %d (sec)", entry.wait_services_sec);
733 m_entries.push_back(entry);
736 pWakeUp = pWakeUp->NextSiblingElement("wakeup"); // get next one
740 void CWakeOnAccess::SaveToXML()
743 TiXmlElement xmlRootElement("onaccesswakeup");
744 TiXmlNode *pRoot = xmlDoc.InsertEndChild(xmlRootElement);
747 XMLUtils::SetInt(pRoot, "netinittimeout", m_netinit_sec);
748 XMLUtils::SetInt(pRoot, "netsettletime", m_netsettle_ms);
750 for (EntriesVector::const_iterator i = m_entries.begin(); i != m_entries.end(); ++i)
752 TiXmlElement xmlSetting("wakeup");
753 TiXmlNode* pWakeUpNode = pRoot->InsertEndChild(xmlSetting);
756 XMLUtils::SetString(pWakeUpNode, "host", i->host);
757 XMLUtils::SetString(pWakeUpNode, "mac", i->mac);
758 XMLUtils::SetInt(pWakeUpNode, "pingport", i->ping_port);
759 XMLUtils::SetInt(pWakeUpNode, "pingmode", i->ping_mode);
760 XMLUtils::SetInt(pWakeUpNode, "timeout", GetTotalSeconds(i->timeout));
761 XMLUtils::SetInt(pWakeUpNode, "waitonline", i->wait_online1_sec);
762 XMLUtils::SetInt(pWakeUpNode, "waitonline2", i->wait_online2_sec);
763 XMLUtils::SetInt(pWakeUpNode, "waitservices", i->wait_services_sec);
767 xmlDoc.SaveFile(GetSettingFile());