From 025da58b793747619dc3dbebc6cf709d4022d17e Mon Sep 17 00:00:00 2001 From: SimulPiscator Date: Tue, 6 Feb 2024 19:47:41 +0100 Subject: [PATCH] Run SANE session on a separate thread to avoid concurrent access --- CMakeLists.txt | 1 + basic/workerthread.cpp | 88 ++++++++++++++++++++++++++++++++++++++++++ basic/workerthread.h | 43 +++++++++++++++++++++ server/scanjob.cpp | 43 ++++++++++++++++++--- 4 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 basic/workerthread.cpp create mode 100644 basic/workerthread.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5aff6c7..de5eb60 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,7 @@ add_executable(${PROJECT_NAME} basic/uuid.cpp basic/dictionary.cpp basic/fdbuf.cpp + basic/workerthread.cpp web/httpserver.cpp web/webpage.cpp web/errorpage.cpp diff --git a/basic/workerthread.cpp b/basic/workerthread.cpp new file mode 100644 index 0000000..15aabdb --- /dev/null +++ b/basic/workerthread.cpp @@ -0,0 +1,88 @@ +/* +AirSane Imaging Daemon +Copyright (C) 2018-2023 Simul Piscator + +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 3 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, see . +*/ +#include "workerthread.h" + +#include +#include +#include +#include +#include + +struct WorkerThread::Private +{ + std::mutex mMutex; + std::condition_variable mThreadCondition, mExecuteCondition; + Callable* mpCallable = nullptr; + bool mCallDone = false; + bool mStarted = false; + bool mTerminate = false; + + std::thread mThread; + void threadFunc(); +}; + +WorkerThread::WorkerThread() + : p(new Private) +{ + std::unique_lock lock(p->mMutex); + p->mStarted = false; + p->mThread = std::thread([this](){ p->threadFunc(); }); + p->mThreadCondition.wait(lock, [this](){ return p->mStarted; }); +} + +WorkerThread::~WorkerThread() +{ + std::unique_lock lock(p->mMutex); + p->mTerminate = true; + lock.unlock(); + p->mExecuteCondition.notify_one(); + if (p->mThread.joinable()) + p->mThread.join(); + delete p; +} + +void WorkerThread::executeSynchronously(Callable& c) +{ + std::unique_lock lock(p->mMutex); + assert(p->mpCallable == nullptr); + p->mpCallable = &c; + p->mCallDone = false; + p->mExecuteCondition.notify_one(); + p->mThreadCondition.wait(lock, [this](){ return p->mCallDone; }); +} + +void WorkerThread::Private::threadFunc() +{ + std::unique_lock lock(mMutex); + mStarted = true; + lock.unlock(); + mThreadCondition.notify_one(); + while (!mTerminate) + { + std::unique_lock lock(mMutex); + mExecuteCondition.wait(lock, [this](){ return mTerminate || mpCallable; }); + if (mpCallable) { + assert(!mCallDone); + mpCallable->onCall(); + mpCallable = nullptr; + mCallDone = true; + lock.unlock(); + mThreadCondition.notify_one(); + } + } +} diff --git a/basic/workerthread.h b/basic/workerthread.h new file mode 100644 index 0000000..a530550 --- /dev/null +++ b/basic/workerthread.h @@ -0,0 +1,43 @@ +/* +AirSane Imaging Daemon +Copyright (C) 2018-2023 Simul Piscator + +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 3 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, see . +*/ +#ifndef WORKER_THREAD_H +#define WORKER_THREAD_H + +class WorkerThread +{ +public: + WorkerThread(); + ~WorkerThread(); + + WorkerThread(const WorkerThread&) = delete; + WorkerThread& operator=(const WorkerThread&) = delete; + + struct Callable + { + virtual ~Callable() {} + virtual void onCall() = 0; + }; + void executeSynchronously(Callable&); + +private: + struct Private; + Private* p; +}; + + +#endif // WORKER_THREAD_H diff --git a/server/scanjob.cpp b/server/scanjob.cpp index f15383d..72ab331 100644 --- a/server/scanjob.cpp +++ b/server/scanjob.cpp @@ -22,6 +22,7 @@ along with this program. If not, see . #include "imageformats/pngencoder.h" #include "scanner.h" #include "web/httpserver.h" +#include "basic/workerthread.h" #include #include @@ -104,21 +105,29 @@ struct ScanJob::Private Scanner* mpScanner; std::string mUuid; - ::time_t mCreated, mLastActive; + std::atomic<::time_t> mCreated, mLastActive; std::atomic mState; std::atomic mStateReason; - SANE_Status mAdfStatus; + std::atomic mAdfStatus; std::string mScanSource, mIntent, mDocumentFormat, mColorMode; int mBitDepth, mRes_dpi; bool mColorScan; double mLeft_px, mTop_px, mWidth_px, mHeight_px; - int mKind, mImagesCompleted; + std::atomic mKind, mImagesCompleted; std::shared_ptr mpSession; OptionsFile::Options mDeviceOptions; std::vector mGammaTable; + + // We need a job-permanent worker thread to execute + // beginTransfer() and finishTransfer(). + // If these functions are called from two different + // threads (e.g., requests for NextDocument), we get + // into difficulties because backends are not required + // to be thread safe. + WorkerThread mWorkerThread; }; ScanJob::ScanJob(Scanner* scanner, const std::string& uuid) @@ -126,7 +135,7 @@ ScanJob::ScanJob(Scanner* scanner, const std::string& uuid) { p->mpScanner = scanner; p->mCreated = ::time(nullptr); - p->mLastActive = p->mCreated; + p->mLastActive = p->mCreated.load(); p->mUuid = uuid; p->mState = pending; p->mStateReason = PWG_NONE; @@ -543,7 +552,18 @@ ScanJob::writeJobInfoXml(std::ostream& os) const bool ScanJob::beginTransfer() { - return p->beginTransfer(); + struct : WorkerThread::Callable + { + void onCall() override + { + result = p->beginTransfer(); + } + bool result = false; + Private* p = nullptr; + } functionCall; + functionCall.p = p; + p->mWorkerThread.executeSynchronously(functionCall); + return functionCall.result; } bool @@ -629,7 +649,18 @@ ScanJob::Private::closeSession() ScanJob& ScanJob::finishTransfer(std::ostream& os) { - p->finishTransfer(os); + struct : WorkerThread::Callable + { + void onCall() override + { + p->finishTransfer(*pOs); + } + Private* p = nullptr; + std::ostream* pOs = nullptr; + } functionCall; + functionCall.p = p; + functionCall.pOs = &os; + p->mWorkerThread.executeSynchronously(functionCall); return *this; }