316 lines
11 KiB
C++
316 lines
11 KiB
C++
/*
|
|
* Copyright (C) 2013 Emeric Poupon
|
|
*
|
|
* This file is part of LMS.
|
|
*
|
|
* LMS 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.
|
|
*
|
|
* LMS 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 LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <thread>
|
|
|
|
#include <boost/asio/io_context.hpp>
|
|
#include <boost/property_tree/xml_parser.hpp>
|
|
|
|
#include <Wt/WServer.h>
|
|
#include <Wt/WApplication.h>
|
|
|
|
#include "services/auth/IAuthTokenService.hpp"
|
|
#include "services/auth/IPasswordService.hpp"
|
|
#include "services/auth/IEnvService.hpp"
|
|
#include "services/cover/ICoverService.hpp"
|
|
#include "services/database/Db.hpp"
|
|
#include "services/database/Session.hpp"
|
|
#include "services/recommendation/IRecommendationService.hpp"
|
|
#include "services/scanner/IScannerService.hpp"
|
|
#include "services/scrobbling/IScrobblingService.hpp"
|
|
#include "subsonic/SubsonicResource.hpp"
|
|
#include "ui/LmsApplication.hpp"
|
|
#include "ui/LmsApplicationManager.hpp"
|
|
#include "utils/IChildProcessManager.hpp"
|
|
#include "utils/IConfig.hpp"
|
|
#include "utils/IOContextRunner.hpp"
|
|
#include "utils/Service.hpp"
|
|
#include "utils/String.hpp"
|
|
#include "utils/WtLogger.hpp"
|
|
|
|
static
|
|
std::size_t
|
|
getThreadCount()
|
|
{
|
|
const unsigned long configHttpServerThreadCount {Service<IConfig>::get()->getULong("http-server-thread-count", 0)};
|
|
|
|
// Reserve at least 2 threads since we still have some blocking IO (for example when reading from ffmpeg)
|
|
return configHttpServerThreadCount ? configHttpServerThreadCount : std::max<unsigned long>(2, std::thread::hardware_concurrency());
|
|
}
|
|
|
|
static
|
|
std::vector<std::string>
|
|
generateWtConfig(std::string execPath)
|
|
{
|
|
std::vector<std::string> args;
|
|
|
|
const std::filesystem::path wtConfigPath {Service<IConfig>::get()->getPath("working-dir") / "wt_config.xml"};
|
|
const std::filesystem::path wtLogFilePath {Service<IConfig>::get()->getPath("log-file", "/var/log/lms.log")};
|
|
const std::filesystem::path wtAccessLogFilePath {Service<IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log")};
|
|
const std::filesystem::path wtResourcesPath {Service<IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources")};
|
|
|
|
args.push_back(execPath);
|
|
args.push_back("--config=" + wtConfigPath.string());
|
|
args.push_back("--docroot=" + std::string {Service<IConfig>::get()->getString("docroot")});
|
|
args.push_back("--approot=" + std::string {Service<IConfig>::get()->getString("approot")});
|
|
args.push_back("--deploy-path=" + std::string {Service<IConfig>::get()->getString("deploy-path", "/")});
|
|
if (!wtResourcesPath.empty())
|
|
args.push_back("--resources-dir=" + wtResourcesPath.string());
|
|
|
|
if (Service<IConfig>::get()->getBool("tls-enable", false))
|
|
{
|
|
args.push_back("--https-port=" + std::to_string( Service<IConfig>::get()->getULong("listen-port", 5082)));
|
|
args.push_back("--https-address=" + std::string {Service<IConfig>::get()->getString("listen-addr", "0.0.0.0")});
|
|
args.push_back("--ssl-certificate=" + std::string {Service<IConfig>::get()->getString("tls-cert")});
|
|
args.push_back("--ssl-private-key=" + std::string {Service<IConfig>::get()->getString("tls-key")});
|
|
args.push_back("--ssl-tmp-dh=" + std::string {Service<IConfig>::get()->getString("tls-dh")});
|
|
}
|
|
else
|
|
{
|
|
args.push_back("--http-port=" + std::to_string( Service<IConfig>::get()->getULong("listen-port", 5082)));
|
|
args.push_back("--http-address=" + std::string {Service<IConfig>::get()->getString("listen-addr", "0.0.0.0")});
|
|
}
|
|
|
|
if (!wtAccessLogFilePath.empty())
|
|
args.push_back("--accesslog=" + wtAccessLogFilePath.string());
|
|
|
|
args.push_back("--threads=" + std::to_string(getThreadCount()));
|
|
|
|
// Generate the wt_config.xml file
|
|
boost::property_tree::ptree pt;
|
|
|
|
pt.put("server.application-settings.<xmlattr>.location", "*");
|
|
pt.put("server.application-settings.log-file", wtLogFilePath.string());
|
|
pt.put("server.application-settings.log-config", Service<IConfig>::get()->getString("log-config", "* -debug -info:WebRequest"));
|
|
pt.put("server.application-settings.behind-reverse-proxy", Service<IConfig>::get()->getBool("behind-reverse-proxy", false));
|
|
|
|
{
|
|
boost::property_tree::ptree viewport;
|
|
viewport.put("<xmlattr>.name", "viewport");
|
|
viewport.put("<xmlattr>.content", "width=device-width, initial-scale=1, user-scalable=no");
|
|
pt.add_child("server.application-settings.head-matter.meta", viewport);
|
|
}
|
|
{
|
|
boost::property_tree::ptree themeColor;
|
|
themeColor.put("<xmlattr>.name", "theme-color");
|
|
themeColor.put("<xmlattr>.content", "#303030");
|
|
pt.add_child("server.application-settings.head-matter.meta", themeColor);
|
|
}
|
|
|
|
|
|
{
|
|
std::ofstream oss {wtConfigPath.string().c_str(), std::ios::out};
|
|
if (!oss)
|
|
throw LmsException {"Can't open '" + wtConfigPath.string() + "' for writing!"};
|
|
|
|
boost::property_tree::xml_parser::write_xml(oss, pt);
|
|
|
|
if (!oss)
|
|
throw LmsException {"Can't write in file '" + wtConfigPath.string() + "', no space left?"};
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
|
|
static
|
|
void
|
|
proxyScannerEventsToApplication(Scanner::IScannerService& scanner, Wt::WServer& server)
|
|
{
|
|
auto postAll {[](Wt::WServer& server, std::function<void()> cb)
|
|
{
|
|
server.postAll([cb = std::move(cb)]
|
|
{
|
|
// may be nullptr, see https://redmine.webtoolkit.eu/issues/8202
|
|
if (LmsApp)
|
|
cb();
|
|
});
|
|
}};
|
|
|
|
scanner.getEvents().scanStarted.connect([&]
|
|
{
|
|
postAll(server, []
|
|
{
|
|
LmsApp->getScannerEvents().scanStarted.emit();
|
|
LmsApp->triggerUpdate();
|
|
});
|
|
});
|
|
|
|
scanner.getEvents().scanComplete.connect([&] (const Scanner::ScanStats& stats)
|
|
{
|
|
postAll(server, [=]
|
|
{
|
|
LmsApp->getScannerEvents().scanComplete.emit(stats);
|
|
LmsApp->triggerUpdate();
|
|
});
|
|
});
|
|
|
|
scanner.getEvents().scanInProgress.connect([&] (const Scanner::ScanStepStats& stats)
|
|
{
|
|
postAll(server, [=]
|
|
{
|
|
LmsApp->getScannerEvents().scanInProgress.emit(stats);
|
|
LmsApp->triggerUpdate();
|
|
});
|
|
});
|
|
|
|
scanner.getEvents().scanScheduled.connect([&] (const Wt::WDateTime dateTime)
|
|
{
|
|
postAll(server, [=]
|
|
{
|
|
LmsApp->getScannerEvents().scanScheduled.emit(dateTime);
|
|
LmsApp->triggerUpdate();
|
|
});
|
|
});
|
|
}
|
|
|
|
int main(int argc, char* argv[])
|
|
{
|
|
std::filesystem::path configFilePath {"/etc/lms.conf"};
|
|
int res {EXIT_FAILURE};
|
|
|
|
assert(argc > 0);
|
|
assert(argv[0] != NULL);
|
|
|
|
if (argc == 2)
|
|
configFilePath = std::string(argv[1], 0, 256);
|
|
else if (argc > 2)
|
|
{
|
|
std::cerr << "Usage:\t" << argv[0] << "\t[conf_file]\n\n"
|
|
<< "Options:\n"
|
|
<< "\tconf_file:\t path to the LMS configuration file (defaults to " << configFilePath << ")\n\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
try
|
|
{
|
|
close(STDIN_FILENO);
|
|
|
|
Service<IConfig> config {createConfig(configFilePath)};
|
|
Service<Logger> logger {std::make_unique<WtLogger>()};
|
|
|
|
// Make sure the working directory exists
|
|
std::filesystem::create_directories(config->getPath("working-dir"));
|
|
std::filesystem::create_directories(config->getPath("working-dir") / "cache");
|
|
|
|
// Construct WT configuration and get the argc/argv back
|
|
const std::vector<std::string> wtServerArgs {generateWtConfig(argv[0])};
|
|
|
|
std::vector<const char*> wtArgv(wtServerArgs.size());
|
|
for (std::size_t i = 0; i < wtServerArgs.size(); ++i)
|
|
{
|
|
std::cout << "ARG = " << wtServerArgs[i] << std::endl;
|
|
wtArgv[i] = wtServerArgs[i].c_str();
|
|
}
|
|
|
|
boost::asio::io_context ioContext; // ioContext used to dispatch all the services that are out of the Wt event loop
|
|
Wt::WServer server {argv[0]};
|
|
server.setServerConfiguration(wtServerArgs.size(), const_cast<char**>(&wtArgv[0]));
|
|
|
|
IOContextRunner ioContextRunner {ioContext, getThreadCount()};
|
|
|
|
// Initializing a connection pool to the database that will be shared along services
|
|
Database::Db database {config->getPath("working-dir") / "lms.db", getThreadCount()};
|
|
{
|
|
Database::Session session {database};
|
|
session.prepareTables();
|
|
session.optimize();
|
|
}
|
|
|
|
UserInterface::LmsApplicationManager appManager;
|
|
|
|
// Service initialization order is important (reverse-order for deinit)
|
|
Service<IChildProcessManager> childProcessManagerService {createChildProcessManager(ioContext)};
|
|
Service<Auth::IAuthTokenService> authTokenService;
|
|
Service<Auth::IPasswordService> authPasswordService;
|
|
Service<Auth::IEnvService> authEnvService;
|
|
|
|
const std::string authenticationBackend {StringUtils::stringToLower(config->getString("authentication-backend", "internal"))};
|
|
if (authenticationBackend == "internal" || authenticationBackend == "pam")
|
|
{
|
|
authTokenService.assign(Auth::createAuthTokenService(database, config->getULong("login-throttler-max-entriees", 10000)));
|
|
authPasswordService.assign(Auth::createPasswordService(authenticationBackend, database, config->getULong("login-throttler-max-entriees", 10000), *authTokenService.get()));
|
|
}
|
|
else if (authenticationBackend == "http-headers")
|
|
{
|
|
authEnvService.assign(Auth::createEnvService(authenticationBackend, database));
|
|
}
|
|
else
|
|
throw LmsException {"Bad value '" + authenticationBackend + "' for 'authentication-backend'"};
|
|
|
|
Service<Cover::ICoverService> coverService {Cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg")};
|
|
Service<Recommendation::IRecommendationService> recommendationService {Recommendation::createRecommendationService(database)};
|
|
Service<Scanner::IScannerService> scannerService {Scanner::createScannerService(database, *recommendationService)};
|
|
|
|
scannerService->getEvents().scanComplete.connect([&]
|
|
{
|
|
// Flush cover cache even if no changes:
|
|
// covers may be external files that changed and we don't keep track of them
|
|
coverService->flushCache();
|
|
});
|
|
|
|
Service<Scrobbling::IScrobblingService> scrobblingService {Scrobbling::createScrobblingService(ioContext, database)};
|
|
|
|
std::unique_ptr<Wt::WResource> subsonicResource;
|
|
|
|
// bind API resources
|
|
if (config->getBool("api-subsonic", true))
|
|
{
|
|
subsonicResource = API::Subsonic::createSubsonicResource(database);
|
|
server.addResource(subsonicResource.get(), "rest/");
|
|
}
|
|
|
|
// bind UI entry point
|
|
server.addEntryPoint(Wt::EntryPointType::Application,
|
|
[&](const Wt::WEnvironment &env)
|
|
{
|
|
return UserInterface::LmsApplication::create(env, database, appManager);
|
|
});
|
|
|
|
proxyScannerEventsToApplication(*scannerService, server);
|
|
|
|
LMS_LOG(MAIN, INFO) << "Starting server...";
|
|
server.start();
|
|
|
|
LMS_LOG(MAIN, INFO) << "Now running...";
|
|
Wt::WServer::waitForShutdown();
|
|
|
|
LMS_LOG(MAIN, INFO) << "Stopping server...";
|
|
server.stop();
|
|
|
|
LMS_LOG(MAIN, INFO) << "Quitting...";
|
|
res = EXIT_SUCCESS;
|
|
}
|
|
catch (const Wt::WServer::Exception& e)
|
|
{
|
|
LMS_LOG(MAIN, FATAL) << "Caught WServer::Exception: " << e.what();
|
|
std::cerr << "Caught a WServer::Exception: " << e.what() << std::endl;
|
|
res = EXIT_FAILURE;
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
LMS_LOG(MAIN, FATAL) << "Caught std::exception: " << e.what();
|
|
std::cerr << "Caught std::exception: " << e.what() << std::endl;
|
|
res = EXIT_FAILURE;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|