Added support for external writers

This commit is contained in:
Marco van Dijk 2022-09-14 15:01:51 +02:00 committed by Thulinma
parent 6921586622
commit 2b18a414b4
15 changed files with 510 additions and 154 deletions

View file

@ -845,6 +845,7 @@ add_custom_command(OUTPUT generated/server.html.h
# MistController - Build #
########################################
add_executable(MistController
src/controller/controller_external_writers.h
src/controller/controller_limits.h
src/controller/controller_uplink.h
src/controller/controller_api.h
@ -857,6 +858,7 @@ add_executable(MistController
src/controller/controller_push.h
src/controller/controller_variables.h
src/controller/controller.cpp
src/controller/controller_external_writers.cpp
src/controller/controller_updater.cpp
src/controller/controller_streams.cpp
src/controller/controller_storage.cpp
@ -906,6 +908,9 @@ add_test(URLTest COMMAND urltest)
add_executable(logtest test/log.cpp ${BINARY_DIR}/mist/.headers)
target_link_libraries(logtest mist)
add_test(LOGTest COMMAND logtest)
add_executable(logconvertertest test/converter.cpp ${BINARY_DIR}/mist/.headers)
target_link_libraries(logconvertertest mist)
add_test(LOGConverterTest COMMAND logconvertertest)
add_executable(downloadertest test/downloader.cpp ${BINARY_DIR}/mist/.headers)
target_link_libraries(downloadertest mist)
add_test(DownloaderTest COMMAND downloadertest)

View file

@ -206,6 +206,10 @@ static inline void show_stackframe(){}
#define CUSTOM_VARIABLES_INITSIZE 64 * 1024
#define EXTWRITERS "MstExtWriters"
#define EXTWRITERS_INITSIZE 1 * 1024 * 1024
#define SEM_STATISTICS "/MstStat"
#define SEM_USERS "/MstUser%s" //%s stream name

View file

@ -950,12 +950,8 @@ namespace DTSC{
inFile.read(scanBuf, fileSize);
inFile.close();
size_t offset = 8;
if (!memcmp(scanBuf, "DTP2", 4)){offset = 20;}
DTSC::Scan src(scanBuf + offset, fileSize - offset);
reInit(_streamName, src);
DTSC::Packet pkt(scanBuf, fileSize, true);
reInit(_streamName, pkt.getScan());
free(scanBuf);
}
@ -2693,134 +2689,6 @@ namespace DTSC{
return result;
}
/// Writes the current Meta object in DTSH format to the given filename
void Meta::toFile(const std::string &fName) const{
std::string lVars;
size_t lVarSize = 0;
if (inputLocalVars.size()){
lVars = inputLocalVars.toString();
lVarSize = 2 + 14 + 5 + lVars.size();
}
std::ofstream oFile(fName.c_str(), std::ios::binary | std::ios::ate);
oFile.write(DTSC::Magic_Header, 4);
oFile.write(c32(lVarSize + getSendLen() - 8), 4);
oFile.write("\340", 1);
if (getVod()){oFile.write("\000\003vod\001\000\000\000\000\000\000\000\001", 14);}
if (getLive()){oFile.write("\000\004live\001\000\000\000\000\000\000\000\001", 15);}
oFile.write("\000\007version\001", 10);
oFile.write(c64(DTSH_VERSION), 8);
if (lVarSize){
oFile.write("\000\016inputLocalVars\002", 17);
oFile.write(c32(lVars.size()), 4);
oFile.write(lVars.data(), lVars.size());
}
oFile.write("\000\006tracks\340", 9);
for (std::map<size_t, Track>::const_iterator it = tracks.begin(); it != tracks.end(); it++){
if (!it->second.parts.getPresent()){continue;}
std::string tmp = getTrackIdentifier(it->first, true);
oFile.write(c16(tmp.size()), 2);
oFile.write(tmp.data(), tmp.size());
oFile.write("\340", 1); // Begin track object
size_t fragCount = it->second.fragments.getPresent();
oFile.write("\000\011fragments\002", 12);
oFile.write(c32(fragCount * DTSH_FRAGMENT_SIZE), 4);
for (size_t i = 0; i < fragCount; i++){
oFile.write(c32(it->second.fragments.getInt("duration", i)), 4);
oFile.put(it->second.fragments.getInt("keys", i));
oFile.write(c32(it->second.fragments.getInt("firstkey", i) + 1), 4);
oFile.write(c32(it->second.fragments.getInt("size", i)), 4);
}
size_t keyCount = it->second.keys.getPresent();
oFile.write("\000\004keys\002", 7);
oFile.write(c32(keyCount * DTSH_KEY_SIZE), 4);
for (size_t i = 0; i < keyCount; i++){
oFile.write(c64(it->second.keys.getInt("bpos", i)), 8);
oFile.write(c24(it->second.keys.getInt("duration", i)), 3);
oFile.write(c32(it->second.keys.getInt("number", i) + 1), 4);
oFile.write(c16(it->second.keys.getInt("parts", i)), 2);
oFile.write(c64(it->second.keys.getInt("time", i)), 8);
}
oFile.write("\000\010keysizes\002,", 11);
oFile.write(c32(keyCount * 4), 4);
for (size_t i = 0; i < keyCount; i++){
oFile.write(c32(it->second.keys.getInt("size", i)), 4);
}
size_t partCount = it->second.parts.getPresent();
oFile.write("\000\005parts\002", 8);
oFile.write(c32(partCount * DTSH_PART_SIZE), 4);
for (size_t i = 0; i < partCount; i++){
oFile.write(c24(it->second.parts.getInt("size", i)), 3);
oFile.write(c24(it->second.parts.getInt("duration", i)), 3);
oFile.write(c24(it->second.parts.getInt("offset", i)), 3);
}
oFile.write("\000\007trackid\001", 10);
oFile.write(c64(it->second.track.getInt("id")), 8);
if (it->second.track.getInt("missedFrags")){
oFile.write("\000\014missed_frags\001", 15);
oFile.write(c64(it->second.track.getInt("missedFrags")), 8);
}
oFile.write("\000\007firstms\001", 10);
oFile.write(c64(it->second.track.getInt("firstms")), 8);
oFile.write("\000\006lastms\001", 9);
oFile.write(c64(it->second.track.getInt("lastms")), 8);
oFile.write("\000\003bps\001", 6);
oFile.write(c64(it->second.track.getInt("bps")), 8);
oFile.write("\000\006maxbps\001", 9);
oFile.write(c64(it->second.track.getInt("maxbps")), 8);
tmp = getInit(it->first);
oFile.write("\000\004init\002", 7);
oFile.write(c32(tmp.size()), 4);
oFile.write(tmp.data(), tmp.size());
tmp = getCodec(it->first);
oFile.write("\000\005codec\002", 8);
oFile.write(c32(tmp.size()), 4);
oFile.write(tmp.data(), tmp.size());
tmp = getLang(it->first);
if (tmp.size() && tmp != "und"){
oFile.write("\000\004lang\002", 7);
oFile.write(c32(tmp.size()), 4);
oFile.write(tmp.data(), tmp.size());
}
tmp = getType(it->first);
oFile.write("\000\004type\002", 7);
oFile.write(c32(tmp.size()), 4);
oFile.write(tmp.data(), tmp.size());
if (tmp == "audio"){
oFile.write("\000\004rate\001", 7);
oFile.write(c64(it->second.track.getInt("rate")), 8);
oFile.write("\000\004size\001", 7);
oFile.write(c64(it->second.track.getInt("size")), 8);
oFile.write("\000\010channels\001", 11);
oFile.write(c64(it->second.track.getInt("channels")), 8);
}else if (tmp == "video"){
oFile.write("\000\005width\001", 8);
oFile.write(c64(it->second.track.getInt("width")), 8);
oFile.write("\000\006height\001", 9);
oFile.write(c64(it->second.track.getInt("height")), 8);
oFile.write("\000\004fpks\001", 7);
oFile.write(c64(it->second.track.getInt("fpks")), 8);
}
oFile.write("\000\000\356", 3); // End this track Object
}
oFile.write("\000\000\356", 3); // End tracks object
oFile.write("\000\000\356", 3); // End global object
oFile.close();
}
/// Converts the current Meta object to JSON format
void Meta::toJSON(JSON::Value &res, bool skipDynamic, bool tracksOnly) const{
res.null();
@ -2885,10 +2753,29 @@ namespace DTSC{
if (getSource() != ""){res["source"] = getSource();}
}
/// Writes the current Meta object in DTSH format to the given uri
void Meta::toFile(const std::string &uri) const{
// Create writing socket
int outFd = -1;
if (!Util::externalWriter(uri, outFd, false)){return;}
Socket::Connection outFile(outFd, -1);
if (outFile){
send(outFile, false, getValidTracks(), false);
outFile.close();
}
}
/// Sends the current Meta object through a socket in DTSH format
void Meta::send(Socket::Connection &conn, bool skipDynamic, std::set<size_t> selectedTracks, bool reID) const{
std::string lVars;
size_t lVarSize = 0;
if (inputLocalVars.size()){
lVars = inputLocalVars.toString();
lVarSize = 2 + 14 + 5 + lVars.size();
}
conn.SendNow(DTSC::Magic_Header, 4);
conn.SendNow(c32(getSendLen(skipDynamic, selectedTracks) - 8), 4);
conn.SendNow(c32(lVarSize + getSendLen(skipDynamic, selectedTracks) - 8), 4);
conn.SendNow("\340", 1);
if (getVod()){conn.SendNow("\000\003vod\001\000\000\000\000\000\000\000\001", 14);}
if (getLive()){conn.SendNow("\000\004live\001\000\000\000\000\000\000\000\001", 15);}
@ -2898,6 +2785,11 @@ namespace DTSC{
conn.SendNow("\000\010unixzero\001", 11);
conn.SendNow(c64(Util::unixMS() - Util::bootMS() + getBootMsOffset()), 8);
}
if (lVarSize){
conn.SendNow("\000\016inputLocalVars\002", 17);
conn.SendNow(c32(lVars.size()), 4);
conn.SendNow(lVars.data(), lVars.size());
}
conn.SendNow("\000\006tracks\340", 9);
for (std::set<size_t>::const_iterator it = selectedTracks.begin(); it != selectedTracks.end(); it++){
std::string tmp = getTrackIdentifier(*it, true);
@ -2923,7 +2815,7 @@ namespace DTSC{
conn.SendNow(c32(fragments.getInt("duration", i + fragBegin)), 4);
conn.SendNow(std::string(1, (char)fragments.getInt("keys", i + fragBegin)));
conn.SendNow(c32(fragments.getInt("firstkey", i + fragBegin)), 4);
conn.SendNow(c32(fragments.getInt("firstkey", i + fragBegin) + 1), 4);
conn.SendNow(c32(fragments.getInt("size", i + fragBegin)), 4);
}

View file

@ -465,7 +465,7 @@ namespace DTSC{
void remap(const std::string &_streamName = "");
uint64_t getSendLen(bool skipDynamic = false, std::set<size_t> selectedTracks = std::set<size_t>()) const;
void toFile(const std::string &fName) const;
void toFile(const std::string &uri) const;
void send(Socket::Connection &conn, bool skypDynamic = false,
std::set<size_t> selectedTracks = std::set<size_t>(), bool reID = false) const;
void toJSON(JSON::Value &res, bool skipDynamic = true, bool tracksOnly = false) const;

View file

@ -7,6 +7,7 @@
#include "procs.h"
#include "timing.h"
#include "util.h"
#include "url.h"
#include <errno.h> // errno, ENOENT, EEXIST
#include <iomanip>
#include <iostream>
@ -186,7 +187,104 @@ namespace Util{
}
val = val.substr(startPos, length);
}
/// \brief splits a string on commas and returns a list of substrings
void splitString(std::string &src, char delim, std::deque<std::string> &result){
result.clear();
std::deque<uint64_t> positions;
uint64_t pos = src.find(delim, 0);
while (pos != std::string::npos){
positions.push_back(pos);
pos = src.find(delim, pos + 1);
}
if (positions.size() == 0){
result.push_back(src);
return;
}
uint64_t prevPos = 0;
for (int i = 0; i < positions.size(); i++) {
if (!prevPos){result.push_back(src.substr(prevPos, positions[i]));}
else{result.push_back(src.substr(prevPos + 1, positions[i] - prevPos - 1));}
prevPos = positions[i];
}
if (prevPos < src.size()){result.push_back(src.substr(prevPos + 1));}
}
/// \brief Connects the given file descriptor to a file or uploader binary
/// \param uri target URL or filepath
/// \param outFile file descriptor which will be used to send data
/// \param append whether to open this connection in truncate or append mode
bool externalWriter(const std::string & uri, int &outFile, bool append){
HTTP::URL target(uri);
// If it is a remote target, we might need to spawn an external binary
if (!target.isLocalPath()){
bool matchedProtocol = false;
// Read configured external writers
IPC::sharedPage extwriPage(EXTWRITERS, 0, false, false);
if (extwriPage.mapped){
Util::RelAccX extWri(extwriPage.mapped, false);
if (extWri.isReady()){
for (uint64_t i = 0; i < extWri.getEndPos(); i++){
// Retrieve binary config
std::string name = extWri.getPointer("name", i);
std::string cmdline = extWri.getPointer("cmdline", i);
Util::RelAccX protocols = Util::RelAccX(extWri.getPointer("protocols", i));
uint8_t protocolCount = protocols.getPresent();
JSON::Value protocolArray;
for (uint8_t idx = 0; idx < protocolCount; idx++){
protocolArray.append(protocols.getPointer("protocol", idx));
}
jsonForEach(protocolArray, protocol){
if (target.protocol != (*protocol).asStringRef()){ continue; }
HIGH_MSG("Using %s in order connect to URL with protocol %s", name.c_str(), target.protocol.c_str());
matchedProtocol = true;
// Split configured parameters for this writer on whitespace
// TODO: we might want to trim whitespaces and remove empty parameters
std::deque<std::string> parameterList;
Util::splitString(cmdline, ' ', parameterList);
// Build the startup command, which needs space for the program name, each parameter, the target url and a null at the end
char **cmd = (char**)malloc(sizeof(char*) * (parameterList.size() + 2));
size_t curArg = 0;
// Write each parameter
for (size_t j = 0; j < parameterList.size(); j++) {
cmd[curArg++] = (char*)parameterList[j].c_str();
}
// Write the target URL as the last positional argument then close with a null at the end
cmd[curArg++] = (char*)uri.c_str();
cmd[curArg++] = 0;
pid_t child = startConverted(cmd, outFile);
if (child == -1){
ERROR_MSG("'%s' process did not start, aborting", cmd[0]);
return false;
}
Util::Procs::forget(child);
free(cmd);
break;
}
if (matchedProtocol){ break; }
}
}
}
if (!matchedProtocol){
ERROR_MSG("Could not connect to '%s', since we do not have a configured external writer to handle '%s' protocols", uri.c_str(), target.protocol.c_str());
return false;
}
}else{
int flags = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
int mode = O_RDWR | O_CREAT | (append ? O_APPEND : O_TRUNC);
if (!Util::createPathFor(uri)){
ERROR_MSG("Cannot not create file %s: could not create parent folder", uri.c_str());
return false;
}
outFile = open(uri.c_str(), mode, flags);
if (outFile < 0){
ERROR_MSG("Failed to open file %s, error: %s", uri.c_str(), strerror(errno));
return false;
}
}
return true;
}
//Returns the time to wait in milliseconds for exponential back-off waiting.
//If currIter > maxIter, always returns 5ms to prevent tight eternal loops when mistakes are made
//Otherwise, exponentially increases wait time for a total of maxWait milliseconds after maxIter calls.
@ -337,6 +435,118 @@ namespace Util{
}
}
/// \brief Forks to a log converter, which spawns an external writer and pretty prints it stdout and stderr
pid_t startConverted(const char *const *argv, int &outFile){
int p[2];
if (pipe(p) == -1){
ERROR_MSG("Unable to create pipe in order to connect to the STDIN of the target binary");
return -1;
}
Util::Procs::fork_prepare();
pid_t converterPid = fork();
// Child process
if (converterPid == 0){
Util::Procs::fork_complete();
close(p[1]);
// Override signals
struct sigaction new_action;
new_action.sa_handler = SIG_IGN;
sigemptyset(&new_action.sa_mask);
new_action.sa_flags = 0;
sigaction(SIGINT, &new_action, NULL);
sigaction(SIGHUP, &new_action, NULL);
sigaction(SIGTERM, &new_action, NULL);
sigaction(SIGPIPE, &new_action, NULL);
// Start external writer
int fdOut = -1;
int fdErr = -1;
pid_t binPid = Util::Procs::StartPiped(argv, &p[0], &fdOut, &fdErr);
close(p[0]);
if (binPid == -1){
FAIL_MSG("Failed to start binary `%s`", argv[0]);
}
// Close all sockets in the socketList
for (std::set<int>::iterator it = Util::Procs::socketList.begin();
it != Util::Procs::socketList.end(); ++it){
close(*it);
}
// Okay, so.... hear me out here.
// This code normalizes the stdin and stdout file descriptors, so that
// they are connected to the pipes we're reading from the child process.
// This is not technically needed, but it makes it easier to debug pipe-
// related problems, and works around a nasty issue were left-over stdout
// connected to another converted process may keep that other process alive
// when it really shouldn't.
// This isn't pretty, it's not fully correct, but it's good enough for now.
// If somebody writes a prettier version of this, please do sent a pull request.
// Thanks <3
std::set<int> toClose;
while (fdErr == 0 || fdErr == 1){
int tmp = dup(fdErr);
if (tmp > -1){
toClose.insert(fdErr);
fdErr = tmp;
}
}
while (fdOut == 0 || fdOut == 1){
int tmp = dup(fdOut);
if (tmp > -1){
toClose.insert(fdOut);
fdOut = tmp;
}
}
while (toClose.size()){
close(*toClose.begin());
toClose.erase(toClose.begin());
}
dup2(fdErr, 1);
dup2(fdOut, 0);
close(fdErr);
close(fdOut);
// Read pipes and write to the stdErr of parent process
Util::logConverter(1, 0, 2, argv[0], binPid);
exit(0);
}
if (converterPid == -1){
FAIL_MSG("Failed to fork log converter for log handling!");
close(p[1]);
}else{
Util::Procs::remember(converterPid);
outFile = p[1];
}
close(p[0]);
Util::Procs::fork_complete();
return converterPid;
}
void logConverter(int inErr, int inOut, int out, const char *progName, pid_t pid){
Socket::Connection errStream(-1, inErr);
Socket::Connection outStream(-1, inOut);
errStream.setBlocking(false);
outStream.setBlocking(false);
while (errStream || outStream){
if (errStream.spool() || errStream.Received().size()){
while (errStream.Received().size()){
std::string &line = errStream.Received().get();
while (line.find('\r') != std::string::npos){line.erase(line.find('\r'));}
while (line.find('\n') != std::string::npos){line.erase(line.find('\n'));}
dprintf(out, "INFO|%s|%d||%s|%s\n", progName, pid, Util::streamName, line.c_str());
line.clear();
}
}else if (outStream.spool() || outStream.Received().size()){
while (outStream.Received().size()){
std::string &line = outStream.Received().get();
while (line.find('\r') != std::string::npos){line.erase(line.find('\r'));}
while (line.find('\n') != std::string::npos){line.erase(line.find('\n'));}
dprintf(out, "INFO|%s|%d||%s|%s\n", progName, pid, Util::streamName, line.c_str());
line.clear();
}
}else{Util::sleep(25);}
}
errStream.close();
outStream.close();
}
/// Parses log messages from the given file descriptor in, printing them to out, optionally
/// calling the given callback for each valid message. Closes the file descriptor on read error
void logParser(int in, int out, bool colored,

View file

@ -15,6 +15,8 @@ namespace Util{
void stringToLower(std::string &val);
size_t replace(std::string &str, const std::string &from, const std::string &to);
void stringTrim(std::string &val);
void splitString(std::string &val, char delim, std::deque<std::string> &result);
bool externalWriter(const std::string & file, int &outFile, bool append = false);
int64_t expBackoffMs(const size_t currIter, const size_t maxIter, const int64_t maxWait);
@ -64,6 +66,8 @@ namespace Util{
void logParser(int in, int out, bool colored,
void callback(const std::string &, const std::string &, const std::string &, uint64_t, bool) = 0);
void redirectLogsIfNeeded();
pid_t startConverted(const char *const *argv, int &outFile);
void logConverter(int inErr, int inOut, int out, const char *progName, pid_t pid);
/// Holds type, size and offset for RelAccX class internal data fields.
class RelAccXFieldData{

View file

@ -2,6 +2,7 @@
/// Contains all code for the controller executable.
#include "controller_api.h"
#include "controller_external_writers.h"
#include "controller_capabilities.h"
#include "controller_connectors.h"
#include "controller_push.h"
@ -586,6 +587,9 @@ int main_loop(int argc, char **argv){
#endif
/*LTS-END*/
// Init external writer config
Controller::externalWritersToShm();
// start main loop
while (Controller::conf.is_active){
Controller::conf.serveThreadedSocket(Controller::handleAPIConnection);

View file

@ -4,6 +4,7 @@
#include "controller_statistics.h"
#include "controller_storage.h"
#include "controller_streams.h"
#include "controller_external_writers.h"
#include <dirent.h> //for browse API call
#include <fstream>
#include <mist/auth.h>
@ -1203,6 +1204,13 @@ void Controller::handleAPICommands(JSON::Value &Request, JSON::Value &Response){
if (Request.isMember("variable_add")){Controller::addVariable(Request["variable_add"], Response["variable_list"]);}
if (Request.isMember("variable_remove")){Controller::removeVariable(Request["variable_remove"], Response["variable_list"]);}
if (Request.isMember("external_writer_remove")){Controller::removeExternalWriter(Request["external_writer_remove"]);}
if (Request.isMember("external_writer_add")){Controller::addExternalWriter(Request["external_writer_add"]);}
if (Request.isMember("external_writer_remove") || Request.isMember("external_writer_add") ||
Request.isMember("external_writer_list")){
Controller::listExternalWriters(Response["external_writer_list"]);
}
Controller::writeConfig();
Controller::configChanged = false;

View file

@ -0,0 +1,173 @@
#include "controller_external_writers.h"
#include "controller_statistics.h"
#include "controller_storage.h"
#include <mist/downloader.h>
#include <mist/bitfields.h>
#include <mist/config.h>
#include <mist/json.h>
#include <mist/stream.h>
#include <mist/triggers.h>
#include <string>
namespace Controller{
// Size of the shared memory page
static uint64_t pageSize = EXTWRITERS_INITSIZE;
/// \brief Writes external writers from the server config to shared memory
void externalWritersToShm(){
uint64_t writerCount = Controller::Storage["extwriters"].size();
IPC::sharedPage writersPage(EXTWRITERS, pageSize, false, false);
// If we have an existing page, set the reload flag
if (writersPage.mapped){
writersPage.master = true;
Util::RelAccX binAccx = Util::RelAccX(writersPage.mapped, false);
// Check if we need a bigger page
uint64_t sizeRequired = binAccx.getOffset() + binAccx.getRSize() * writerCount;
if (pageSize < sizeRequired){pageSize = sizeRequired;}
binAccx.setReload();
}
// Close & unlink any existing page and create a new one
writersPage.close();
writersPage.init(EXTWRITERS, pageSize, true, false);
Util::RelAccX exwriAccx = Util::RelAccX(writersPage.mapped, false);
exwriAccx = Util::RelAccX(writersPage.mapped, false);
exwriAccx.addField("name", RAX_32STRING);
exwriAccx.addField("cmdline", RAX_256STRING);
exwriAccx.addField("protocols", RAX_NESTED, RAX_64STRING * writerCount * 8);
// Set amount of records that can fit and how many will be used
uint64_t reqCount = (pageSize - exwriAccx.getOffset()) / exwriAccx.getRSize();
exwriAccx.setRCount(reqCount);
exwriAccx.setPresent(reqCount);
exwriAccx.setEndPos(writerCount);
// Do the same for the nested protocol field
uint64_t index = 0;
jsonForEach(Controller::Storage["extwriters"], it){
std::string name = (*it)[0u].asString();
std::string cmdline = (*it)[1u].asString();
exwriAccx.setString("name", name, index);
exwriAccx.setString("cmdline", cmdline, index);
// Create nested field for source match
uint8_t protocolCount = (*it)[2u].size();
Util::RelAccX protocolAccx = Util::RelAccX(exwriAccx.getPointer("protocols", index), false);
protocolAccx.addField("protocol", RAX_64STRING);
protocolAccx.setRCount(protocolCount);
protocolAccx.setPresent(protocolCount);
protocolAccx.setEndPos(protocolCount);
uint8_t binIt = 0;
jsonForEach((*it)[2u], protIt){
std::string thisProtocol = (*protIt).asString();
protocolAccx.setString("protocol", thisProtocol, binIt);
binIt++;
}
index++;
protocolAccx.setReady();
}
exwriAccx.setReady();
// Leave the page in memory after returning
writersPage.master = false;
}
/// \brief Adds a new generic writer binary to the server config
/// The request should contain:
/// - name: name given to this binary in order to edit/remove it's entry in Mists config
/// - cmdline: command line including arguments
/// - supported URL protocols: used to identify for what targets we need to run the executable
void addExternalWriter(JSON::Value &request){
std::string name;
std::string cmdline;
JSON::Value protocols;
bool isNew = true;
if (request.isArray()){
if(request.size() == 4){
name = request[0u].asString();
cmdline = request[1u].asString();
protocols = request[2u];
}else{
ERROR_MSG("Cannot add external writer, as the request contained %u variables. Required variables are: name, cmdline and protocols", request.size());
return;
}
}else{
name = request["name"].asString();
cmdline = request["cmdline"].asString();
protocols = request["protocols"];
}
//convert protocols from string to array if needed
if (protocols.isString()){protocols.append(protocols.asString());}
if (!name.size()){
ERROR_MSG("Blank or missing name in request");
return;
}
if (!cmdline.size()){
ERROR_MSG("Blank or missing cmdline in request");
return;
}
if (!protocols.size()){
ERROR_MSG("Missing protocols in request");
return;
}
if (name.size() > 31){
name = name.substr(0, 31);
WARN_MSG("Maximum name length is 31 characters, truncating name to '%s'", name.c_str());
}
if (cmdline.size() > 255){
cmdline.erase(255);
WARN_MSG("Maximum cmdline length is 255 characters, truncating cmdline to '%s'", cmdline.c_str());
}
jsonForEach(protocols, protIt){
if ((*protIt).size() > 63){
(*protIt) = (*protIt).asString().substr(0, 63);
WARN_MSG("Maximum protocol length is 63 characters, truncating protocol to '%s'", (*protIt).asStringRef().c_str());
}
}
// Check if we have an existing variable with the same name to modify
jsonForEach(Controller::Storage["extwriters"], it){
if ((*it)[0u].asString() == name){
INFO_MSG("Modifying existing external writer '%s'", name.c_str());
(*it)[1u] = cmdline;
(*it)[2u] = protocols;
isNew = false;
break;
}
}
// Else push a new custom variable to the list
if (isNew){
INFO_MSG("Adding new external writer '%s'", name.c_str());
JSON::Value thisVar;
thisVar.append(name);
thisVar.append(cmdline);
thisVar.append(protocols);
Controller::Storage["extwriters"].append(thisVar);
}
// Modify shm
externalWritersToShm();
}
/// \brief Fills output with all defined external writers
void listExternalWriters(JSON::Value &output){
output = Controller::Storage["extwriters"];
}
/// \brief Removes the external writer name contained in the request from shm and the sever config
void removeExternalWriter(const JSON::Value &request){
std::string name;
if (request.isString()){
name = request.asStringRef();
}else if (request.isArray()){
name = request[0u].asStringRef();
}else if (request.isMember("name")){
name = request["name"].asStringRef();
}
if (!name.size()){
WARN_MSG("Aborting request to remove an external writer, as no name was given");
return;
}
// Modify config
jsonForEach(Controller::Storage["extwriters"], it){
if ((*it)[0u].asString() == name){it.remove();}
}
// Modify shm
externalWritersToShm();
}
}// namespace Controller

View file

@ -0,0 +1,14 @@
#include <mist/config.h>
#include <mist/json.h>
#include <mist/tinythread.h>
#include <string>
namespace Controller{
// API calls to manage external writers
void addExternalWriter(JSON::Value &request);
void listExternalWriters(JSON::Value &output);
void removeExternalWriter(const JSON::Value &request);
// internal use only
void externalWritersToShm();
}// namespace Controller

View file

@ -3,6 +3,7 @@ executables += {
'name': 'MistController',
'sources' : [
files( 'controller.cpp',
'controller_external_writers.cpp',
'controller_updater.cpp',
'controller_streams.cpp',
'controller_storage.cpp',

View file

@ -8,11 +8,11 @@
#include <iterator>
#include <mist/auth.h>
#include <mist/defines.h>
#include <mist/downloader.h>
#include <mist/encode.h>
#include <mist/procs.h>
#include <mist/stream.h>
#include <mist/triggers.h>
#include <mist/urireader.h>
#include <sstream>
#include <sys/wait.h>
@ -587,6 +587,7 @@ namespace Mist{
}
// close file
file.close();
// Export DTSH to a file or remote target
outMeta.toFile(fileName + ".dtsh");
}
@ -1496,7 +1497,20 @@ namespace Mist{
return true;
}
}
meta.reInit(config->getString("streamname"), config->getString("input") + ".dtsh");
// Try to read any existing DTSH file
std::string fileName = config->getString("input") + ".dtsh";
HIGH_MSG("Loading metadata for stream '%s' from file '%s'", streamName.c_str(), fileName.c_str());
char *scanBuf;
uint64_t fileSize;
HTTP::URIReader inFile(fileName);
if (!inFile){return false;}
inFile.readAll(scanBuf, fileSize);
inFile.close();
if (!fileSize){return false;}
DTSC::Packet pkt(scanBuf, fileSize, true);
HIGH_MSG("Retrieved header of %lu bytes", fileSize);
meta.reInit(streamName, pkt.getScan());
if (meta.version != DTSH_VERSION){
INFO_MSG("Updating wrong version header file from version %u to %u", meta.version, DTSH_VERSION);
return false;

View file

@ -2287,24 +2287,20 @@ namespace Mist{
sentHeader = true;
}
/// \brief Makes the generic writer available to output classes
/// \param file target URL or filepath
/// \param append whether to open this connection in truncate or append mode
/// \param conn connection which will be used to send data. Will use Output's internal myConn if not initialised
bool Output::connectToFile(std::string file, bool append, Socket::Connection *conn){
if (!conn){conn = &myConn;}
int flags = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
int mode = O_RDWR | O_CREAT | (append ? O_APPEND : O_TRUNC);
if (!Util::createPathFor(file)){
ERROR_MSG("Cannot not create file %s: could not create parent folder", file.c_str());
return false;
}
int outFile = open(file.c_str(), mode, flags);
if (outFile < 0){
ERROR_MSG("Failed to open file %s, error: %s", file.c_str(), strerror(errno));
return false;
}
if (*conn){
int outFile = -1;
if (!conn) {conn = &myConn;}
bool isFileTarget = HTTP::URL(file).isLocalPath();
if (!Util::externalWriter(file, outFile, append)){return false;}
if (*conn && isFileTarget) {
flock(conn->getSocket(), LOCK_UN | LOCK_NB);
}
// Lock the file in exclusive mode to ensure no other processes write to it
if(flock(outFile, LOCK_EX | LOCK_NB)){
if(isFileTarget && flock(outFile, LOCK_EX | LOCK_NB)){
ERROR_MSG("Failed to lock file %s, error: %s", file.c_str(), strerror(errno));
return false;
}
@ -2319,7 +2315,7 @@ namespace Mist{
int r = dup2(outFile, conn->getSocket());
if (r == -1){
ERROR_MSG("Failed to create an alias for the socket using dup2: %s.", strerror(errno));
ERROR_MSG("Failed to create an alias for the socket %d -> %d using dup2: %s.", outFile, conn->getSocket(), strerror(errno));
return false;
}
close(outFile);

30
test/converter.cpp Normal file
View file

@ -0,0 +1,30 @@
#include <iostream>
#include <mist/procs.h>
#include <mist/util.h>
#include <mist/defines.h>
#include <mist/socket.h>
#include <mist/timing.h>
#include <unistd.h>
int main(int argc, char **argv){
int fd = -1;
Util::printDebugLevel = 10;
pid_t pid = Util::startConverted(argv + 1, fd);
INFO_MSG("PID=%u", pid);
Socket::Connection in(-1, 0);
Socket::Connection logOut(fd, -1);
while (Util::Procs::childRunning(pid)){
if (in.spool()){
while (in.Received().size()){
logOut.SendNow(in.Received().get());
in.Received().get().clear();
}
}
if (!in){
logOut.close();
}
Util::sleep(1000);
}
INFO_MSG("Shutting down");
return 0;
}

View file

@ -2,6 +2,7 @@
# Testing binaries that are not unit tests, but intended for manual use
logtest = executable('logtest', 'log.cpp', dependencies: libmist_dep)
convertertest = executable('convertertest', 'converter.cpp', dependencies: libmist_dep)
downloadertest = executable('downloadertest', 'downloader.cpp', dependencies: libmist_dep)
urireadertest = executable('urireadertest', 'urireader.cpp', dependencies: libmist_dep)
jsontest = executable('jsontest', 'json.cpp', dependencies: libmist_dep)