diff options
Diffstat (limited to 'xbmc/interfaces/generic')
-rw-r--r-- | xbmc/interfaces/generic/CMakeLists.txt | 15 | ||||
-rw-r--r-- | xbmc/interfaces/generic/ILanguageInvocationHandler.h | 31 | ||||
-rw-r--r-- | xbmc/interfaces/generic/ILanguageInvoker.cpp | 96 | ||||
-rw-r--r-- | xbmc/interfaces/generic/ILanguageInvoker.h | 77 | ||||
-rw-r--r-- | xbmc/interfaces/generic/LanguageInvokerThread.cpp | 133 | ||||
-rw-r--r-- | xbmc/interfaces/generic/LanguageInvokerThread.h | 56 | ||||
-rw-r--r-- | xbmc/interfaces/generic/RunningScriptObserver.cpp | 42 | ||||
-rw-r--r-- | xbmc/interfaces/generic/RunningScriptObserver.h | 34 | ||||
-rw-r--r-- | xbmc/interfaces/generic/RunningScriptsHandler.h | 104 | ||||
-rw-r--r-- | xbmc/interfaces/generic/ScriptInvocationManager.cpp | 423 | ||||
-rw-r--r-- | xbmc/interfaces/generic/ScriptInvocationManager.h | 156 | ||||
-rw-r--r-- | xbmc/interfaces/generic/ScriptRunner.cpp | 177 | ||||
-rw-r--r-- | xbmc/interfaces/generic/ScriptRunner.h | 49 |
13 files changed, 1393 insertions, 0 deletions
diff --git a/xbmc/interfaces/generic/CMakeLists.txt b/xbmc/interfaces/generic/CMakeLists.txt new file mode 100644 index 0000000..8fd742d --- /dev/null +++ b/xbmc/interfaces/generic/CMakeLists.txt @@ -0,0 +1,15 @@ +set(SOURCES ILanguageInvoker.cpp + LanguageInvokerThread.cpp + RunningScriptObserver.cpp + ScriptInvocationManager.cpp + ScriptRunner.cpp) + +set(HEADERS ILanguageInvocationHandler.h + ILanguageInvoker.h + LanguageInvokerThread.h + RunningScriptsHandler.h + RunningScriptObserver.h + ScriptInvocationManager.h + ScriptRunner.h) + +core_add_library(generic_interface) diff --git a/xbmc/interfaces/generic/ILanguageInvocationHandler.h b/xbmc/interfaces/generic/ILanguageInvocationHandler.h new file mode 100644 index 0000000..233e20d --- /dev/null +++ b/xbmc/interfaces/generic/ILanguageInvocationHandler.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +class ILanguageInvoker; + +class ILanguageInvocationHandler +{ +public: + ILanguageInvocationHandler() = default; + virtual ~ILanguageInvocationHandler() = default; + + virtual bool Initialize() { return true; } + virtual void Process() { } + virtual void PulseGlobalEvent() { } + virtual void Uninitialize() { } + + virtual bool OnScriptInitialized(ILanguageInvoker *invoker) { return true; } + virtual void OnScriptStarted(ILanguageInvoker *invoker) { } + virtual void NotifyScriptAborting(ILanguageInvoker *invoker) { } + virtual void OnExecutionEnded(ILanguageInvoker* invoker) {} + virtual void OnScriptFinalized(ILanguageInvoker *invoker) { } + + virtual ILanguageInvoker* CreateInvoker() = 0; +}; diff --git a/xbmc/interfaces/generic/ILanguageInvoker.cpp b/xbmc/interfaces/generic/ILanguageInvoker.cpp new file mode 100644 index 0000000..df1193e --- /dev/null +++ b/xbmc/interfaces/generic/ILanguageInvoker.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ILanguageInvoker.h" + +#include "interfaces/generic/ILanguageInvocationHandler.h" + +#include <string> +#include <vector> + +ILanguageInvoker::ILanguageInvoker(ILanguageInvocationHandler *invocationHandler) + : m_id(-1), + m_state(InvokerStateUninitialized), + m_invocationHandler(invocationHandler) +{ } + +ILanguageInvoker::~ILanguageInvoker() = default; + +bool ILanguageInvoker::Execute(const std::string &script, const std::vector<std::string> &arguments /* = std::vector<std::string>() */) +{ + if (m_invocationHandler) + m_invocationHandler->OnScriptStarted(this); + + return execute(script, arguments); +} + +bool ILanguageInvoker::Stop(bool abort /* = false */) +{ + return stop(abort); +} + +bool ILanguageInvoker::IsActive() const +{ + return GetState() > InvokerStateUninitialized && GetState() < InvokerStateScriptDone; +} + +bool ILanguageInvoker::IsRunning() const +{ + return GetState() == InvokerStateRunning; +} + +bool ILanguageInvoker::IsStopping() const +{ + return GetState() == InvokerStateStopping; +} + +void ILanguageInvoker::pulseGlobalEvent() +{ + if (m_invocationHandler) + m_invocationHandler->PulseGlobalEvent(); +} + +bool ILanguageInvoker::onExecutionInitialized() +{ + if (m_invocationHandler == NULL) + return false; + + return m_invocationHandler->OnScriptInitialized(this); +} + +void ILanguageInvoker::AbortNotification() +{ + if (m_invocationHandler) + m_invocationHandler->NotifyScriptAborting(this); +} + +void ILanguageInvoker::onExecutionFailed() +{ + if (m_invocationHandler) + m_invocationHandler->OnExecutionEnded(this); +} + +void ILanguageInvoker::onExecutionDone() +{ + if (m_invocationHandler) + m_invocationHandler->OnExecutionEnded(this); +} + +void ILanguageInvoker::onExecutionFinalized() +{ + if (m_invocationHandler) + m_invocationHandler->OnScriptFinalized(this); +} + +void ILanguageInvoker::setState(InvokerState state) +{ + if (state <= m_state) + return; + + m_state = state; +} diff --git a/xbmc/interfaces/generic/ILanguageInvoker.h b/xbmc/interfaces/generic/ILanguageInvoker.h new file mode 100644 index 0000000..da4001e --- /dev/null +++ b/xbmc/interfaces/generic/ILanguageInvoker.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/IAddon.h" + +#include <memory> +#include <string> +#include <vector> + +class CLanguageInvokerThread; +class ILanguageInvocationHandler; + +typedef enum +{ + InvokerStateUninitialized, + InvokerStateInitialized, + InvokerStateRunning, + InvokerStateStopping, + InvokerStateScriptDone, + InvokerStateExecutionDone, + InvokerStateFailed +} InvokerState; + +class ILanguageInvoker +{ +public: + explicit ILanguageInvoker(ILanguageInvocationHandler *invocationHandler); + virtual ~ILanguageInvoker(); + + virtual bool Execute(const std::string &script, const std::vector<std::string> &arguments = std::vector<std::string>()); + virtual bool Stop(bool abort = false); + virtual bool IsStopping() const; + + void SetId(int id) { m_id = id; } + int GetId() const { return m_id; } + const ADDON::AddonPtr& GetAddon() const { return m_addon; } + void SetAddon(const ADDON::AddonPtr &addon) { m_addon = addon; } + InvokerState GetState() const { return m_state; } + bool IsActive() const; + bool IsRunning() const; + void Reset() { m_state = InvokerStateUninitialized; } + +protected: + friend class CLanguageInvokerThread; + + /** + * Called to notify the script is aborting. + */ + virtual void AbortNotification(); + + virtual bool execute(const std::string &script, const std::vector<std::string> &arguments) = 0; + virtual bool stop(bool abort) = 0; + + virtual void pulseGlobalEvent(); + virtual bool onExecutionInitialized(); + virtual void onExecutionFailed(); + virtual void onExecutionDone(); + virtual void onExecutionFinalized(); + + void setState(InvokerState state); + + ADDON::AddonPtr m_addon; + +private: + int m_id; + InvokerState m_state; + ILanguageInvocationHandler *m_invocationHandler; +}; + +typedef std::shared_ptr<ILanguageInvoker> LanguageInvokerPtr; diff --git a/xbmc/interfaces/generic/LanguageInvokerThread.cpp b/xbmc/interfaces/generic/LanguageInvokerThread.cpp new file mode 100644 index 0000000..67f3d14 --- /dev/null +++ b/xbmc/interfaces/generic/LanguageInvokerThread.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "LanguageInvokerThread.h" + +#include "ScriptInvocationManager.h" + +#include <utility> + +CLanguageInvokerThread::CLanguageInvokerThread(LanguageInvokerPtr invoker, + CScriptInvocationManager* invocationManager, + bool reuseable) + : ILanguageInvoker(NULL), + CThread("LanguageInvoker"), + m_invoker(std::move(invoker)), + m_invocationManager(invocationManager), + m_reusable(reuseable) +{ } + +CLanguageInvokerThread::~CLanguageInvokerThread() +{ + Stop(true); +} + +InvokerState CLanguageInvokerThread::GetState() const +{ + if (m_invoker == NULL) + return InvokerStateFailed; + + return m_invoker->GetState(); +} + +void CLanguageInvokerThread::Release() +{ + m_bStop = true; + m_condition.notify_one(); +} + +bool CLanguageInvokerThread::execute(const std::string &script, const std::vector<std::string> &arguments) +{ + if (m_invoker == NULL || script.empty()) + return false; + + m_script = script; + m_args = arguments; + + if (CThread::IsRunning()) + { + std::unique_lock<std::mutex> lck(m_mutex); + m_restart = true; + m_condition.notify_one(); + } + else + Create(); + + //Todo wait until running + + return true; +} + +bool CLanguageInvokerThread::stop(bool wait) +{ + if (m_invoker == NULL) + return false; + + if (!CThread::IsRunning()) + return false; + + Release(); + + bool result = true; + if (m_invoker->GetState() < InvokerStateExecutionDone) + { + // stop the language-specific invoker + result = m_invoker->Stop(wait); + } + // stop the thread + CThread::StopThread(wait); + + return result; +} + +void CLanguageInvokerThread::OnStartup() +{ + if (m_invoker == NULL) + return; + + m_invoker->SetId(GetId()); + if (m_addon != NULL) + m_invoker->SetAddon(m_addon); +} + +void CLanguageInvokerThread::Process() +{ + if (m_invoker == NULL) + return; + + std::unique_lock<std::mutex> lckdl(m_mutex); + do + { + m_restart = false; + m_invoker->Execute(m_script, m_args); + + if (m_invoker->GetState() != InvokerStateScriptDone) + m_reusable = false; + + m_condition.wait(lckdl, [this] { return m_bStop || m_restart || !m_reusable; }); + + } while (m_reusable && !m_bStop); +} + +void CLanguageInvokerThread::OnExit() +{ + if (m_invoker == NULL) + return; + + m_invoker->onExecutionDone(); + m_invocationManager->OnExecutionDone(GetId()); +} + +void CLanguageInvokerThread::OnException() +{ + if (m_invoker == NULL) + return; + + m_invoker->onExecutionFailed(); + m_invocationManager->OnExecutionDone(GetId()); +}
\ No newline at end of file diff --git a/xbmc/interfaces/generic/LanguageInvokerThread.h b/xbmc/interfaces/generic/LanguageInvokerThread.h new file mode 100644 index 0000000..a1844ba --- /dev/null +++ b/xbmc/interfaces/generic/LanguageInvokerThread.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/generic/ILanguageInvoker.h" +#include "threads/Thread.h" + +#include <string> +#include <vector> + +class CScriptInvocationManager; + +class CLanguageInvokerThread : public ILanguageInvoker, protected CThread +{ +public: + CLanguageInvokerThread(LanguageInvokerPtr invoker, + CScriptInvocationManager* invocationManager, + bool reuseable); + ~CLanguageInvokerThread() override; + + virtual InvokerState GetState() const; + + const std::string& GetScript() const { return m_script; } + LanguageInvokerPtr GetInvoker() const { return m_invoker; } + bool Reuseable(const std::string& script) const + { + return !m_bStop && m_reusable && GetState() == InvokerStateScriptDone && m_script == script; + }; + virtual void Release(); + +protected: + bool execute(const std::string &script, const std::vector<std::string> &arguments) override; + bool stop(bool wait) override; + + void OnStartup() override; + void Process() override; + void OnExit() override; + void OnException() override; + +private: + LanguageInvokerPtr m_invoker; + CScriptInvocationManager *m_invocationManager; + std::string m_script; + std::vector<std::string> m_args; + + std::mutex m_mutex; + std::condition_variable m_condition; + bool m_restart = false; + bool m_reusable = false; +}; diff --git a/xbmc/interfaces/generic/RunningScriptObserver.cpp b/xbmc/interfaces/generic/RunningScriptObserver.cpp new file mode 100644 index 0000000..c89c9f7 --- /dev/null +++ b/xbmc/interfaces/generic/RunningScriptObserver.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2017-2021 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "RunningScriptObserver.h" + +#include "interfaces/generic/ScriptInvocationManager.h" + +using namespace std::chrono_literals; + +CRunningScriptObserver::CRunningScriptObserver(int scriptId, CEvent& evt) + : m_scriptId(scriptId), m_event(evt), m_stopEvent(true), m_thread(this, "ScriptObs") +{ + m_thread.Create(); +} + +CRunningScriptObserver::~CRunningScriptObserver() +{ + Abort(); +} + +void CRunningScriptObserver::Run() +{ + do + { + if (!CScriptInvocationManager::GetInstance().IsRunning(m_scriptId)) + { + m_event.Set(); + break; + } + } while (!m_stopEvent.Wait(20ms)); +} + +void CRunningScriptObserver::Abort() +{ + m_stopEvent.Set(); + m_thread.StopThread(); +} diff --git a/xbmc/interfaces/generic/RunningScriptObserver.h b/xbmc/interfaces/generic/RunningScriptObserver.h new file mode 100644 index 0000000..2622411 --- /dev/null +++ b/xbmc/interfaces/generic/RunningScriptObserver.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2017-2021 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/Event.h" +#include "threads/IRunnable.h" +#include "threads/Thread.h" + +#include <atomic> + +class CRunningScriptObserver : public IRunnable +{ +public: + CRunningScriptObserver(int scriptId, CEvent& evt); + ~CRunningScriptObserver(); + + void Abort(); + +protected: + // implementation of IRunnable + void Run() override; + + int m_scriptId; + CEvent& m_event; + + CEvent m_stopEvent; + CThread m_thread; +}; diff --git a/xbmc/interfaces/generic/RunningScriptsHandler.h b/xbmc/interfaces/generic/RunningScriptsHandler.h new file mode 100644 index 0000000..59f18bb --- /dev/null +++ b/xbmc/interfaces/generic/RunningScriptsHandler.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2017-2021 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/generic/ScriptInvocationManager.h" +#include "interfaces/generic/ScriptRunner.h" +#include "threads/CriticalSection.h" +#include "threads/Event.h" + +#include <cstdint> +#include <map> +#include <mutex> + +template<class TScript> +class CRunningScriptsHandler : protected CScriptRunner +{ +protected: + using HandleType = int; + + CRunningScriptsHandler() = default; + virtual ~CRunningScriptsHandler() = default; + + using CScriptRunner::ExecuteScript; + using CScriptRunner::GetAddon; + using CScriptRunner::SetDone; + using CScriptRunner::StartScript; + + bool RunScript(TScript* script, + const ADDON::AddonPtr& addon, + const std::string& path, + bool resume) + { + if (script == nullptr || addon == nullptr || path.empty()) + return false; + + // reuse an existing script handle or get a new one if necessary + int handle = CScriptInvocationManager::GetInstance().GetReusablePluginHandle(addon->LibPath()); + if (handle < 0) + handle = GetNewScriptHandle(script); + else + ReuseScriptHandle(handle, script); + + // run the script + auto result = CScriptRunner::RunScript(addon, path, handle, resume); + + // remove the script handle if necessary + RemoveScriptHandle(handle); + + return result; + } + + static HandleType GetNewScriptHandle(TScript* script) + { + std::unique_lock<CCriticalSection> lock(s_critical); + uint32_t handle = ++s_scriptHandleCounter; + s_scriptHandles[handle] = script; + + return handle; + } + + static void ReuseScriptHandle(HandleType handle, TScript* script) + { + std::unique_lock<CCriticalSection> lock(s_critical); + s_scriptHandles[handle] = script; + } + + static void RemoveScriptHandle(HandleType handle) + { + std::unique_lock<CCriticalSection> lock(s_critical); + s_scriptHandles.erase(handle); + } + + static TScript* GetScriptFromHandle(HandleType handle) + { + std::unique_lock<CCriticalSection> lock(s_critical); + auto scriptHandle = s_scriptHandles.find(handle); + if (scriptHandle == s_scriptHandles.end()) + return nullptr; + + return scriptHandle->second; + } + + static inline CCriticalSection& GetScriptsLock() { return s_critical; } + +private: + static std::map<HandleType, TScript*> s_scriptHandles; + static CCriticalSection s_critical; + static HandleType s_scriptHandleCounter; +}; + +template<class TScript> +std::map<typename CRunningScriptsHandler<TScript>::HandleType, TScript*> + CRunningScriptsHandler<TScript>::s_scriptHandles; +template<class TScript> +CCriticalSection CRunningScriptsHandler<TScript>::s_critical; +template<class TScript> +typename CRunningScriptsHandler<TScript>::HandleType + CRunningScriptsHandler<TScript>::s_scriptHandleCounter = 0; diff --git a/xbmc/interfaces/generic/ScriptInvocationManager.cpp b/xbmc/interfaces/generic/ScriptInvocationManager.cpp new file mode 100644 index 0000000..9daae0f --- /dev/null +++ b/xbmc/interfaces/generic/ScriptInvocationManager.cpp @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ScriptInvocationManager.h" + +#include "interfaces/generic/ILanguageInvocationHandler.h" +#include "interfaces/generic/ILanguageInvoker.h" +#include "interfaces/generic/LanguageInvokerThread.h" +#include "utils/FileUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" + +#include <cerrno> +#include <mutex> +#include <utility> +#include <vector> + +CScriptInvocationManager::~CScriptInvocationManager() +{ + Uninitialize(); +} + +CScriptInvocationManager& CScriptInvocationManager::GetInstance() +{ + static CScriptInvocationManager s_instance; + return s_instance; +} + +void CScriptInvocationManager::Process() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + // go through all active threads and find and remove all which are done + std::vector<LanguageInvokerThread> tempList; + for (LanguageInvokerThreadMap::iterator it = m_scripts.begin(); it != m_scripts.end(); ) + { + if (it->second.done) + { + tempList.push_back(it->second); + m_scripts.erase(it++); + } + else + ++it; + } + + // remove the finished scripts from the script path map as well + for (const auto& it : tempList) + m_scriptPaths.erase(it.script); + + // we can leave the lock now + lock.unlock(); + + // finally remove the finished threads but we do it outside of any locks in + // case of any callbacks from the destruction of the CLanguageInvokerThread + tempList.clear(); + + // let the invocation handlers do their processing + for (auto& it : m_invocationHandlers) + it.second->Process(); +} + +void CScriptInvocationManager::Uninitialize() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // execute Process() once more to handle the remaining scripts + Process(); + + // it is safe to release early, thread must be in m_scripts too + m_lastInvokerThread = nullptr; + + // make sure all scripts are done + std::vector<LanguageInvokerThread> tempList; + for (const auto& script : m_scripts) + tempList.push_back(script.second); + + m_scripts.clear(); + m_scriptPaths.clear(); + + // we can leave the lock now + lock.unlock(); + + // finally stop and remove the finished threads but we do it outside of any + // locks in case of any callbacks from the stop or destruction logic of + // CLanguageInvokerThread or the ILanguageInvoker implementation + for (auto& it : tempList) + { + if (!it.done) + it.thread->Stop(true); + } + + lock.lock(); + + tempList.clear(); + + // uninitialize all invocation handlers and then remove them + for (auto& it : m_invocationHandlers) + it.second->Uninitialize(); + + m_invocationHandlers.clear(); +} + +void CScriptInvocationManager::RegisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler, const std::string &extension) +{ + if (invocationHandler == NULL || extension.empty()) + return; + + std::string ext = extension; + StringUtils::ToLower(ext); + if (!StringUtils::StartsWithNoCase(ext, ".")) + ext = "." + ext; + + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_invocationHandlers.find(ext) != m_invocationHandlers.end()) + return; + + m_invocationHandlers.insert(std::make_pair(extension, invocationHandler)); + + bool known = false; + for (const auto& it : m_invocationHandlers) + { + if (it.second == invocationHandler) + { + known = true; + break; + } + } + + // automatically initialize the invocation handler if it's a new one + if (!known) + invocationHandler->Initialize(); +} + +void CScriptInvocationManager::RegisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler, const std::set<std::string> &extensions) +{ + if (invocationHandler == NULL || extensions.empty()) + return; + + for (const auto& extension : extensions) + RegisterLanguageInvocationHandler(invocationHandler, extension); +} + +void CScriptInvocationManager::UnregisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler) +{ + if (invocationHandler == NULL) + return; + + std::unique_lock<CCriticalSection> lock(m_critSection); + // get all extensions of the given language invoker + for (std::map<std::string, ILanguageInvocationHandler*>::iterator it = m_invocationHandlers.begin(); it != m_invocationHandlers.end(); ) + { + if (it->second == invocationHandler) + m_invocationHandlers.erase(it++); + else + ++it; + } + + // automatically uninitialize the invocation handler + invocationHandler->Uninitialize(); +} + +bool CScriptInvocationManager::HasLanguageInvoker(const std::string &script) const +{ + std::string extension = URIUtils::GetExtension(script); + StringUtils::ToLower(extension); + + std::unique_lock<CCriticalSection> lock(m_critSection); + std::map<std::string, ILanguageInvocationHandler*>::const_iterator it = m_invocationHandlers.find(extension); + return it != m_invocationHandlers.end() && it->second != NULL; +} + +int CScriptInvocationManager::GetReusablePluginHandle(const std::string& script) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_lastInvokerThread) + { + if (m_lastInvokerThread->Reuseable(script)) + return m_lastPluginHandle; + m_lastInvokerThread->Release(); + m_lastInvokerThread = nullptr; + } + return -1; +} + +LanguageInvokerPtr CScriptInvocationManager::GetLanguageInvoker(const std::string& script) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_lastInvokerThread) + { + if (m_lastInvokerThread->Reuseable(script)) + { + CLog::Log(LOGDEBUG, "{} - Reusing LanguageInvokerThread {} for script {}", __FUNCTION__, + m_lastInvokerThread->GetId(), script); + m_lastInvokerThread->GetInvoker()->Reset(); + return m_lastInvokerThread->GetInvoker(); + } + m_lastInvokerThread->Release(); + m_lastInvokerThread = nullptr; + } + + std::string extension = URIUtils::GetExtension(script); + StringUtils::ToLower(extension); + + std::map<std::string, ILanguageInvocationHandler*>::const_iterator it = m_invocationHandlers.find(extension); + if (it != m_invocationHandlers.end() && it->second != NULL) + return LanguageInvokerPtr(it->second->CreateInvoker()); + + return LanguageInvokerPtr(); +} + +int CScriptInvocationManager::ExecuteAsync( + const std::string& script, + const ADDON::AddonPtr& addon /* = ADDON::AddonPtr() */, + const std::vector<std::string>& arguments /* = std::vector<std::string>() */, + bool reuseable /* = false */, + int pluginHandle /* = -1 */) +{ + if (script.empty()) + return -1; + + if (!CFileUtils::Exists(script, false)) + { + CLog::Log(LOGERROR, "{} - Not executing non-existing script {}", __FUNCTION__, script); + return -1; + } + + LanguageInvokerPtr invoker = GetLanguageInvoker(script); + return ExecuteAsync(script, invoker, addon, arguments, reuseable, pluginHandle); +} + +int CScriptInvocationManager::ExecuteAsync( + const std::string& script, + const LanguageInvokerPtr& languageInvoker, + const ADDON::AddonPtr& addon /* = ADDON::AddonPtr() */, + const std::vector<std::string>& arguments /* = std::vector<std::string>() */, + bool reuseable /* = false */, + int pluginHandle /* = -1 */) +{ + if (script.empty() || languageInvoker == NULL) + return -1; + + if (!CFileUtils::Exists(script, false)) + { + CLog::Log(LOGERROR, "{} - Not executing non-existing script {}", __FUNCTION__, script); + return -1; + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_lastInvokerThread && m_lastInvokerThread->GetInvoker() == languageInvoker) + { + if (addon != NULL) + m_lastInvokerThread->SetAddon(addon); + + // After we leave the lock, m_lastInvokerThread can be released -> copy! + CLanguageInvokerThreadPtr invokerThread = m_lastInvokerThread; + lock.unlock(); + invokerThread->Execute(script, arguments); + + return invokerThread->GetId(); + } + + m_lastInvokerThread = + CLanguageInvokerThreadPtr(new CLanguageInvokerThread(languageInvoker, this, reuseable)); + if (m_lastInvokerThread == NULL) + return -1; + + if (addon != NULL) + m_lastInvokerThread->SetAddon(addon); + + m_lastInvokerThread->SetId(m_nextId++); + m_lastPluginHandle = pluginHandle; + + LanguageInvokerThread thread = {m_lastInvokerThread, script, false}; + m_scripts.insert(std::make_pair(m_lastInvokerThread->GetId(), thread)); + m_scriptPaths.insert(std::make_pair(script, m_lastInvokerThread->GetId())); + // After we leave the lock, m_lastInvokerThread can be released -> copy! + CLanguageInvokerThreadPtr invokerThread = m_lastInvokerThread; + lock.unlock(); + invokerThread->Execute(script, arguments); + + return invokerThread->GetId(); +} + +int CScriptInvocationManager::ExecuteSync( + const std::string& script, + const ADDON::AddonPtr& addon /* = ADDON::AddonPtr() */, + const std::vector<std::string>& arguments /* = std::vector<std::string>() */, + uint32_t timeoutMs /* = 0 */, + bool waitShutdown /* = false */) +{ + if (script.empty()) + return -1; + + if (!CFileUtils::Exists(script, false)) + { + CLog::Log(LOGERROR, "{} - Not executing non-existing script {}", __FUNCTION__, script); + return -1; + } + + LanguageInvokerPtr invoker = GetLanguageInvoker(script); + return ExecuteSync(script, invoker, addon, arguments, timeoutMs, waitShutdown); +} + +int CScriptInvocationManager::ExecuteSync( + const std::string& script, + const LanguageInvokerPtr& languageInvoker, + const ADDON::AddonPtr& addon /* = ADDON::AddonPtr() */, + const std::vector<std::string>& arguments /* = std::vector<std::string>() */, + uint32_t timeoutMs /* = 0 */, + bool waitShutdown /* = false */) +{ + int scriptId = ExecuteAsync(script, languageInvoker, addon, arguments); + if (scriptId < 0) + return -1; + + bool timeout = timeoutMs > 0; + while ((!timeout || timeoutMs > 0) && IsRunning(scriptId)) + { + unsigned int sleepMs = 100U; + if (timeout && timeoutMs < sleepMs) + sleepMs = timeoutMs; + + KODI::TIME::Sleep(std::chrono::milliseconds(sleepMs)); + + if (timeout) + timeoutMs -= sleepMs; + } + + if (IsRunning(scriptId)) + { + Stop(scriptId, waitShutdown); + return ETIMEDOUT; + } + + return 0; +} + +bool CScriptInvocationManager::Stop(int scriptId, bool wait /* = false */) +{ + if (scriptId < 0) + return false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + CLanguageInvokerThreadPtr invokerThread = getInvokerThread(scriptId).thread; + if (invokerThread == NULL) + return false; + + return invokerThread->Stop(wait); +} + +void CScriptInvocationManager::StopRunningScripts(bool wait /* = false */) +{ + for (auto& it : m_scripts) + { + if (!it.second.done) + Stop(it.second.script, wait); + } +} + +bool CScriptInvocationManager::Stop(const std::string &scriptPath, bool wait /* = false */) +{ + if (scriptPath.empty()) + return false; + + std::unique_lock<CCriticalSection> lock(m_critSection); + std::map<std::string, int>::const_iterator script = m_scriptPaths.find(scriptPath); + if (script == m_scriptPaths.end()) + return false; + + return Stop(script->second, wait); +} + +bool CScriptInvocationManager::IsRunning(int scriptId) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + LanguageInvokerThread invokerThread = getInvokerThread(scriptId); + if (invokerThread.thread == NULL) + return false; + + return !invokerThread.done; +} + +bool CScriptInvocationManager::IsRunning(const std::string& scriptPath) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + auto it = m_scriptPaths.find(scriptPath); + if (it == m_scriptPaths.end()) + return false; + + return IsRunning(it->second); +} + +void CScriptInvocationManager::OnExecutionDone(int scriptId) +{ + if (scriptId < 0) + return; + + std::unique_lock<CCriticalSection> lock(m_critSection); + LanguageInvokerThreadMap::iterator script = m_scripts.find(scriptId); + if (script != m_scripts.end()) + script->second.done = true; +} + +CScriptInvocationManager::LanguageInvokerThread CScriptInvocationManager::getInvokerThread(int scriptId) const +{ + if (scriptId < 0) + return LanguageInvokerThread(); + + LanguageInvokerThreadMap::const_iterator script = m_scripts.find(scriptId); + if (script == m_scripts.end()) + return LanguageInvokerThread(); + + return script->second; +} diff --git a/xbmc/interfaces/generic/ScriptInvocationManager.h b/xbmc/interfaces/generic/ScriptInvocationManager.h new file mode 100644 index 0000000..6e409b8 --- /dev/null +++ b/xbmc/interfaces/generic/ScriptInvocationManager.h @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2013-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/IAddon.h" +#include "interfaces/generic/ILanguageInvoker.h" +#include "threads/CriticalSection.h" + +#include <map> +#include <memory> +#include <set> +#include <vector> + +class CLanguageInvokerThread; +typedef std::shared_ptr<CLanguageInvokerThread> CLanguageInvokerThreadPtr; + +class CScriptInvocationManager +{ +public: + static CScriptInvocationManager& GetInstance(); + + void Process(); + void Uninitialize(); + + void RegisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler, const std::string &extension); + void RegisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler, const std::set<std::string> &extensions); + void UnregisterLanguageInvocationHandler(ILanguageInvocationHandler *invocationHandler); + bool HasLanguageInvoker(const std::string &script) const; + LanguageInvokerPtr GetLanguageInvoker(const std::string& script); + + /*! + * \brief Returns addon_handle if last reusable invoker is ready to use. + */ + int GetReusablePluginHandle(const std::string& script); + + /*! + * \brief Executes the given script asynchronously in a separate thread. + * + * \param script Path to the script to be executed + * \param addon (Optional) Addon to which the script belongs + * \param arguments (Optional) List of arguments passed to the script + * \return -1 if an error occurred, otherwise the ID of the script + */ + int ExecuteAsync(const std::string& script, + const ADDON::AddonPtr& addon = ADDON::AddonPtr(), + const std::vector<std::string>& arguments = std::vector<std::string>(), + bool reuseable = false, + int pluginHandle = -1); + /*! + * \brief Executes the given script asynchronously in a separate thread. + * + * \param script Path to the script to be executed + * \param languageInvoker Language invoker to be used to execute the script + * \param addon (Optional) Addon to which the script belongs + * \param arguments (Optional) List of arguments passed to the script + * \return -1 if an error occurred, otherwise the ID of the script + */ + int ExecuteAsync(const std::string& script, + const LanguageInvokerPtr& languageInvoker, + const ADDON::AddonPtr& addon = ADDON::AddonPtr(), + const std::vector<std::string>& arguments = std::vector<std::string>(), + bool reuseable = false, + int pluginHandle = -1); + + /*! + * \brief Executes the given script synchronously. + * + * \details The script is actually executed asynchronously but the calling + * thread is blocked until either the script has finished or the given timeout + * has expired. If the given timeout has expired the script's execution is + * stopped and depending on the specified wait behaviour we wait for the + * script's execution to finish or not. + * + * \param script Path to the script to be executed + * \param addon (Optional) Addon to which the script belongs + * \param arguments (Optional) List of arguments passed to the script + * \param timeout (Optional) Timeout (in milliseconds) for the script's execution + * \param waitShutdown (Optional) Whether to wait when having to forcefully stop the script's execution or not. + * \return -1 if an error occurred, 0 if the script terminated or ETIMEDOUT if the given timeout expired + */ + int ExecuteSync(const std::string& script, + const ADDON::AddonPtr& addon = ADDON::AddonPtr(), + const std::vector<std::string>& arguments = std::vector<std::string>(), + uint32_t timeoutMs = 0, + bool waitShutdown = false); + /*! + * \brief Executes the given script synchronously. + * + * \details The script is actually executed asynchronously but the calling + * thread is blocked until either the script has finished or the given timeout + * has expired. If the given timeout has expired the script's execution is + * stopped and depending on the specified wait behaviour we wait for the + * script's execution to finish or not. + * + * \param script Path to the script to be executed + * \param languageInvoker Language invoker to be used to execute the script + * \param addon (Optional) Addon to which the script belongs + * \param arguments (Optional) List of arguments passed to the script + * \param timeout (Optional) Timeout (in milliseconds) for the script's execution + * \param waitShutdown (Optional) Whether to wait when having to forcefully stop the script's execution or not. + * \return -1 if an error occurred, 0 if the script terminated or ETIMEDOUT if the given timeout expired + */ + int ExecuteSync(const std::string& script, + const LanguageInvokerPtr& languageInvoker, + const ADDON::AddonPtr& addon = ADDON::AddonPtr(), + const std::vector<std::string>& arguments = std::vector<std::string>(), + uint32_t timeoutMs = 0, + bool waitShutdown = false); + bool Stop(int scriptId, bool wait = false); + bool Stop(const std::string &scriptPath, bool wait = false); + + /*! + *\brief Stop all running scripts + *\param wait if kodi should wait for each script to finish (default false) + */ + void StopRunningScripts(bool wait = false); + + bool IsRunning(int scriptId) const; + bool IsRunning(const std::string& scriptPath) const; + +protected: + friend class CLanguageInvokerThread; + + void OnExecutionDone(int scriptId); + +private: + CScriptInvocationManager() = default; + CScriptInvocationManager(const CScriptInvocationManager&) = delete; + CScriptInvocationManager const& operator=(CScriptInvocationManager const&) = delete; + virtual ~CScriptInvocationManager(); + + typedef struct { + CLanguageInvokerThreadPtr thread; + std::string script; + bool done; + } LanguageInvokerThread; + typedef std::map<int, LanguageInvokerThread> LanguageInvokerThreadMap; + typedef std::map<std::string, ILanguageInvocationHandler*> LanguageInvocationHandlerMap; + + LanguageInvokerThread getInvokerThread(int scriptId) const; + + LanguageInvocationHandlerMap m_invocationHandlers; + LanguageInvokerThreadMap m_scripts; + CLanguageInvokerThreadPtr m_lastInvokerThread; + int m_lastPluginHandle = -1; + + std::map<std::string, int> m_scriptPaths; + int m_nextId = 0; + mutable CCriticalSection m_critSection; +}; diff --git a/xbmc/interfaces/generic/ScriptRunner.cpp b/xbmc/interfaces/generic/ScriptRunner.cpp new file mode 100644 index 0000000..b88dbac --- /dev/null +++ b/xbmc/interfaces/generic/ScriptRunner.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2017-2021 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "ScriptRunner.h" + +#include "ServiceBroker.h" +#include "URL.h" +#include "dialogs/GUIDialogBusy.h" +#include "dialogs/GUIDialogProgress.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "interfaces/generic/RunningScriptObserver.h" +#include "interfaces/generic/ScriptInvocationManager.h" +#include "messaging/ApplicationMessenger.h" +#include "threads/SystemClock.h" +#include "utils/StringUtils.h" +#include "utils/log.h" + +#include <vector> + +using namespace std::chrono_literals; + +ADDON::AddonPtr CScriptRunner::GetAddon() const +{ + return m_addon; +} + +CScriptRunner::CScriptRunner() : m_scriptDone(true) +{ } + +bool CScriptRunner::StartScript(const ADDON::AddonPtr& addon, const std::string& path) +{ + return RunScriptInternal(addon, path, 0, false); +} + +bool CScriptRunner::RunScript(const ADDON::AddonPtr& addon, + const std::string& path, + int handle, + bool resume) +{ + return RunScriptInternal(addon, path, handle, resume, true); +} + +void CScriptRunner::SetDone() +{ + m_scriptDone.Set(); +} + +int CScriptRunner::ExecuteScript(const ADDON::AddonPtr& addon, const std::string& path, bool resume) +{ + return ExecuteScript(addon, path, -1, resume); +} + +int CScriptRunner::ExecuteScript(const ADDON::AddonPtr& addon, + const std::string& path, + int handle, + bool resume) +{ + if (addon == nullptr || path.empty()) + return false; + + CURL url(path); + + // get options and remove them from the URL because we can then use the url + // to generate the base path which is passed to the add-on script + auto options = url.GetOptions(); + url.SetOptions(""); + + // setup our parameters to send the script + std::vector<std::string> argv = {url.Get(), // base path + StringUtils::Format("{:d}", handle), options, + StringUtils::Format("resume:{}", resume)}; + + bool reuseLanguageInvoker = false; + const auto reuseLanguageInvokerIt = addon->ExtraInfo().find("reuselanguageinvoker"); + if (reuseLanguageInvokerIt != addon->ExtraInfo().end()) + reuseLanguageInvoker = reuseLanguageInvokerIt->second == "true"; + + // run the script + CLog::Log(LOGDEBUG, "CScriptRunner: running add-on script {:s}('{:s}', '{:s}', '{:s}')", + addon->Name(), argv[0], argv[1], argv[2]); + int scriptId = CScriptInvocationManager::GetInstance().ExecuteAsync(addon->LibPath(), addon, argv, + reuseLanguageInvoker, handle); + if (scriptId < 0) + CLog::Log(LOGERROR, "CScriptRunner: unable to run add-on script {:s}", addon->Name()); + + return scriptId; +} + +bool CScriptRunner::RunScriptInternal(const ADDON::AddonPtr& addon, + const std::string& path, + int handle, + bool resume, + bool wait /* = true */) +{ + if (addon == nullptr || path.empty()) + return false; + + // reset our wait event + m_scriptDone.Reset(); + + // store the add-on + m_addon = addon; + + int scriptId = ExecuteScript(addon, path, handle, resume); + if (scriptId < 0) + return false; + + // we don't need to wait for the script to end + if (!wait) + return true; + + // wait for our script to finish + return WaitOnScriptResult(scriptId, addon->LibPath(), addon->Name()); +} + +bool CScriptRunner::WaitOnScriptResult(int scriptId, + const std::string& path, + const std::string& name) +{ + bool cancelled = false; + + // Add-on scripts can be called from the main and other threads. If called + // form the main thread, we need to bring up the BusyDialog in order to + // keep the render loop alive + if (CServiceBroker::GetAppMessenger()->IsProcessThread()) + { + if (!m_scriptDone.Wait(20ms)) + { + // observe the script until it's finished while showing the busy dialog + CRunningScriptObserver scriptObs(scriptId, m_scriptDone); + + auto& wm = CServiceBroker::GetGUI()->GetWindowManager(); + if (wm.IsModalDialogTopmost(WINDOW_DIALOG_PROGRESS)) + { + auto progress = wm.GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + if (!progress->WaitOnEvent(m_scriptDone)) + cancelled = true; + } + else if (!CGUIDialogBusy::WaitOnEvent(m_scriptDone, 200)) + cancelled = true; + + scriptObs.Abort(); + } + } + else + { + // wait for the script to finish or be cancelled + while (!IsCancelled() && CScriptInvocationManager::GetInstance().IsRunning(scriptId) && + !m_scriptDone.Wait(20ms)) + ; + + // give the script 30 seconds to exit before we attempt to stop it + XbmcThreads::EndTime<> timer(30s); + while (!timer.IsTimePast() && CScriptInvocationManager::GetInstance().IsRunning(scriptId) && + !m_scriptDone.Wait(20ms)) + ; + } + + if (cancelled || IsCancelled()) + { + // cancel the script + if (scriptId != -1 && CScriptInvocationManager::GetInstance().IsRunning(scriptId)) + { + CLog::Log(LOGDEBUG, "CScriptRunner: cancelling add-on script {:s} (id = {:d})", name, + scriptId); + CScriptInvocationManager::GetInstance().Stop(scriptId); + } + } + + return !cancelled && !IsCancelled() && IsSuccessful(); +} diff --git a/xbmc/interfaces/generic/ScriptRunner.h b/xbmc/interfaces/generic/ScriptRunner.h new file mode 100644 index 0000000..77d511d --- /dev/null +++ b/xbmc/interfaces/generic/ScriptRunner.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2017-2021 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/IAddon.h" +#include "threads/Event.h" + +#include <string> + +class CScriptRunner +{ +protected: + CScriptRunner(); + virtual ~CScriptRunner() = default; + + virtual bool IsSuccessful() const = 0; + virtual bool IsCancelled() const = 0; + + ADDON::AddonPtr GetAddon() const; + + bool StartScript(const ADDON::AddonPtr& addon, const std::string& path); + bool RunScript(const ADDON::AddonPtr& addon, const std::string& path, int handle, bool resume); + + void SetDone(); + + static int ExecuteScript(const ADDON::AddonPtr& addon, const std::string& path, bool resume); + static int ExecuteScript(const ADDON::AddonPtr& addon, + const std::string& path, + int handle, + bool resume); + +private: + bool RunScriptInternal(const ADDON::AddonPtr& addon, + const std::string& path, + int handle, + bool resume, + bool wait = true); + bool WaitOnScriptResult(int scriptId, const std::string& path, const std::string& name); + + ADDON::AddonPtr m_addon; + + CEvent m_scriptDone; +}; |