Added support for external writers
This commit is contained in:
parent
6921586622
commit
2b18a414b4
15 changed files with 510 additions and 154 deletions
|
@ -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
|
||||
|
||||
|
|
164
lib/dtsc.cpp
164
lib/dtsc.cpp
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
210
lib/util.cpp
210
lib/util.cpp
|
@ -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,
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue