Added --fps flag to regulate stream. Added hacks commands.

This commit is contained in:
Gonzalo Exequiel Pedone 2021-05-24 19:47:39 -03:00
parent 64296f0223
commit 0957eb4be6
No known key found for this signature in database
GPG key ID: B8B09E63E9B85BAF
20 changed files with 1228 additions and 34 deletions

View file

@ -19,6 +19,7 @@
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <cmath>
#include <codecvt> #include <codecvt>
#include <csignal> #include <csignal>
#include <cstring> #include <cstring>
@ -61,6 +62,15 @@ namespace AkVCam {
std::string helpString; std::string helpString;
ProgramOptionsFunc func; ProgramOptionsFunc func;
std::vector<CmdParserFlags> flags; std::vector<CmdParserFlags> flags;
bool advanced {false};
CmdParserCommand();
CmdParserCommand(const std::string &command,
const std::string &arguments,
const std::string &helpString,
const ProgramOptionsFunc &func,
const std::vector<CmdParserFlags> flags,
bool advanced);
}; };
class CmdParserPrivate class CmdParserPrivate
@ -74,8 +84,8 @@ namespace AkVCam {
std::string basename(const std::string &path); std::string basename(const std::string &path);
void printFlags(const std::vector<CmdParserFlags> &cmdFlags, void printFlags(const std::vector<CmdParserFlags> &cmdFlags,
size_t indent); size_t indent);
size_t maxCommandLength(); size_t maxCommandLength(bool showAdvancedHelp);
size_t maxArgumentsLength(); size_t maxArgumentsLength(bool showAdvancedHelp);
size_t maxFlagsLength(const std::vector<CmdParserFlags> &flags); size_t maxFlagsLength(const std::vector<CmdParserFlags> &flags);
size_t maxFlagsValueLength(const std::vector<CmdParserFlags> &flags); size_t maxFlagsValueLength(const std::vector<CmdParserFlags> &flags);
size_t maxColumnLength(const StringVector &table, size_t maxColumnLength(const StringVector &table,
@ -126,6 +136,9 @@ namespace AkVCam {
int setLogLevel(const StringMap &flags, const StringVector &args); int setLogLevel(const StringMap &flags, const StringVector &args);
int showClients(const StringMap &flags, const StringVector &args); int showClients(const StringMap &flags, const StringVector &args);
int dumpInfo(const StringMap &flags, const StringVector &args); int dumpInfo(const StringMap &flags, const StringVector &args);
int hacks(const StringMap &flags, const StringVector &args);
int hackInfo(const StringMap &flags, const StringVector &args);
int hack(const StringMap &flags, const StringVector &args);
void loadGenerals(Settings &settings); void loadGenerals(Settings &settings);
VideoFormatMatrix readFormats(Settings &settings); VideoFormatMatrix readFormats(Settings &settings);
std::vector<VideoFormat> readFormat(Settings &settings); std::vector<VideoFormat> readFormat(Settings &settings);
@ -154,6 +167,9 @@ AkVCam::CmdParser::CmdParser()
this->addFlags("", this->addFlags("",
{"-h", "--help"}, {"-h", "--help"},
"Show help."); "Show help.");
this->addFlags("",
{"--help-all"},
"Show advanced help.");
this->addFlags("", this->addFlags("",
{"-v", "--version"}, {"-v", "--version"},
"Show program version."); "Show program version.");
@ -236,6 +252,10 @@ AkVCam::CmdParser::CmdParser()
"DEVICE FORMAT WIDTH HEIGHT", "DEVICE FORMAT WIDTH HEIGHT",
"Read frames from stdin and send them to the device.", "Read frames from stdin and send them to the device.",
AKVCAM_BIND_FUNC(CmdParserPrivate::stream)); AKVCAM_BIND_FUNC(CmdParserPrivate::stream));
this->addFlags("stream",
{"-f", "--fps"},
"FPS",
"Read stream input at a constant frame rate.");
this->addCommand("listen-events", this->addCommand("listen-events",
"", "",
"Keep the manager running and listening to global events.", "Keep the manager running and listening to global events.",
@ -297,6 +317,30 @@ AkVCam::CmdParser::CmdParser()
"", "",
"Show all information in a parseable XML format.", "Show all information in a parseable XML format.",
AKVCAM_BIND_FUNC(CmdParserPrivate::dumpInfo)); AKVCAM_BIND_FUNC(CmdParserPrivate::dumpInfo));
this->addCommand("hacks",
"",
"List system hacks to make the virtual camera work.",
AKVCAM_BIND_FUNC(CmdParserPrivate::hacks),
true);
this->addCommand("hack-info",
"HACK",
"Show hack information.",
AKVCAM_BIND_FUNC(CmdParserPrivate::hackInfo),
true);
this->addFlags("hack-info",
{"-s", "--issafe"},
"Is hack safe?");
this->addFlags("hack-info",
{"-c", "--description"},
"Show hack description.");
this->addCommand("hack",
"HACK PARAMS...",
"Apply system hack.",
AKVCAM_BIND_FUNC(CmdParserPrivate::hack),
true);
this->addFlags("hack",
{"-y", "--yes"},
"Accept all risks and continue anyway.");
} }
AkVCam::CmdParser::~CmdParser() AkVCam::CmdParser::~CmdParser()
@ -374,7 +418,10 @@ int AkVCam::CmdParser::parse(int argc, char **argv)
} }
} }
if (this->d->m_ipcBridge.needsRoot(command->command)) if (this->d->m_ipcBridge.needsRoot(command->command)
|| (command->command == "hack"
&& arguments.size() >= 2
&& this->d->m_ipcBridge.hackNeedsRoot(arguments[1])))
return this->d->m_ipcBridge.sudo(argc, argv); return this->d->m_ipcBridge.sudo(argc, argv);
return command->func(flags, arguments); return command->func(flags, arguments);
@ -388,7 +435,8 @@ void AkVCam::CmdParser::setDefaultFuntion(const ProgramOptionsFunc &func)
void AkVCam::CmdParser::addCommand(const std::string &command, void AkVCam::CmdParser::addCommand(const std::string &command,
const std::string &arguments, const std::string &arguments,
const std::string &helpString, const std::string &helpString,
const ProgramOptionsFunc &func) const ProgramOptionsFunc &func,
bool advanced)
{ {
auto it = auto it =
std::find_if(this->d->m_commands.begin(), std::find_if(this->d->m_commands.begin(),
@ -402,12 +450,14 @@ void AkVCam::CmdParser::addCommand(const std::string &command,
arguments, arguments,
helpString, helpString,
func, func,
{}}); {},
advanced});
} else { } else {
it->command = command; it->command = command;
it->arguments = arguments; it->arguments = arguments;
it->helpString = helpString; it->helpString = helpString;
it->func = func; it->func = func;
it->advanced = advanced;
} }
} }
@ -497,22 +547,24 @@ void AkVCam::CmdParserPrivate::printFlags(const std::vector<CmdParserFlags> &cmd
} }
} }
size_t AkVCam::CmdParserPrivate::maxCommandLength() size_t AkVCam::CmdParserPrivate::maxCommandLength(bool showAdvancedHelp)
{ {
size_t length = 0; size_t length = 0;
for (auto &cmd: this->m_commands) for (auto &cmd: this->m_commands)
length = std::max(cmd.command.size(), length); if (!cmd.advanced || showAdvancedHelp)
length = std::max(cmd.command.size(), length);
return length; return length;
} }
size_t AkVCam::CmdParserPrivate::maxArgumentsLength() size_t AkVCam::CmdParserPrivate::maxArgumentsLength(bool showAdvancedHelp)
{ {
size_t length = 0; size_t length = 0;
for (auto &cmd: this->m_commands) for (auto &cmd: this->m_commands)
length = std::max(cmd.arguments.size(), length); if (!cmd.advanced || showAdvancedHelp)
length = std::max(cmd.arguments.size(), length);
return length; return length;
} }
@ -674,7 +726,9 @@ std::string AkVCam::CmdParserPrivate::flagValue(const AkVCam::StringMap &flags,
int AkVCam::CmdParserPrivate::defaultHandler(const StringMap &flags, int AkVCam::CmdParserPrivate::defaultHandler(const StringMap &flags,
const StringVector &args) const StringVector &args)
{ {
if (flags.empty() || this->containsFlag(flags, "", "-h")) if (flags.empty()
|| this->containsFlag(flags, "", "-h")
|| this->containsFlag(flags, "", "--help-all"))
return this->showHelp(flags, args); return this->showHelp(flags, args);
if (this->containsFlag(flags, "", "-v")) { if (this->containsFlag(flags, "", "-v")) {
@ -707,11 +761,13 @@ int AkVCam::CmdParserPrivate::showHelp(const StringMap &flags,
std::cout << "Commands:" << std::endl; std::cout << "Commands:" << std::endl;
std::cout << std::endl; std::cout << std::endl;
auto maxCmdLen = this->maxCommandLength(); bool showAdvancedHelp = this->containsFlag(flags, "", "--help-all");
auto maxArgsLen = this->maxArgumentsLength(); auto maxCmdLen = this->maxCommandLength(showAdvancedHelp);
auto maxArgsLen = this->maxArgumentsLength(showAdvancedHelp);
for (auto &cmd: this->m_commands) { for (auto &cmd: this->m_commands) {
if (cmd.command.empty()) if (cmd.command.empty()
|| (cmd.advanced && !showAdvancedHelp))
continue; continue;
std::cout << " " std::cout << " "
@ -1230,6 +1286,30 @@ int AkVCam::CmdParserPrivate::stream(const AkVCam::StringMap &flags,
return -1; return -1;
} }
auto fpsStr = this->flagValue(flags, "stream", "-f");
double fps = std::numeric_limits<double>::quiet_NaN();
if (!fpsStr.empty()) {
p = nullptr;
fps = int(strtod(fpsStr.c_str(), &p));
if (*p) {
if (!Fraction::isFraction(fpsStr)) {
std::cerr << "The framerate must be a number or a fraction." << std::endl;
return -1;
}
fps = Fraction(fpsStr).value();
}
if (fps <= 0 || std::isinf(fps)) {
std::cerr << "The framerate is out of range." << std::endl;
return -1;
}
}
VideoFormat fmt(format, int(width), int(height), {{30, 1}}); VideoFormat fmt(format, int(width), int(height), {{30, 1}});
if (!this->m_ipcBridge.deviceStart(deviceId, fmt)) { if (!this->m_ipcBridge.deviceStart(deviceId, fmt)) {
@ -1253,6 +1333,20 @@ int AkVCam::CmdParserPrivate::stream(const AkVCam::StringMap &flags,
_setmode(_fileno(stdin), _O_BINARY); _setmode(_fileno(stdin), _O_BINARY);
#endif #endif
auto clock = [] (const std::chrono::time_point<std::chrono::high_resolution_clock> &since) -> double {
return std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::high_resolution_clock::now() - since).count();
};
const double minThreshold = 0.04;
const double maxThreshold = 0.1;
const double framedupThreshold = 0.1;
const double nosyncThreshold = 10.0;
double lastPts = 0.0;
auto t0 = std::chrono::high_resolution_clock::now();
double drift = 0;
uint64_t i = 0;
do { do {
std::cin.read(reinterpret_cast<char *>(frame.data().data() std::cin.read(reinterpret_cast<char *>(frame.data().data()
+ bufferSize), + bufferSize),
@ -1260,7 +1354,46 @@ int AkVCam::CmdParserPrivate::stream(const AkVCam::StringMap &flags,
bufferSize += size_t(std::cin.gcount()); bufferSize += size_t(std::cin.gcount());
if (bufferSize == frame.data().size()) { if (bufferSize == frame.data().size()) {
this->m_ipcBridge.write(deviceId, frame); if (fpsStr.empty()) {
this->m_ipcBridge.write(deviceId, frame);
} else {
double pts = double(i) / fps;
for (;;) {
double clock_pts = clock(t0) + drift;
double diff = pts - clock_pts;
double delay = pts - lastPts;
double syncThreshold =
std::max(minThreshold,
std::min(delay, maxThreshold));
if (!std::isnan(diff)
&& std::abs(diff) < nosyncThreshold
&& delay < framedupThreshold) {
if (diff <= -syncThreshold) {
lastPts = pts;
break;
}
if (diff > syncThreshold) {
std::this_thread::sleep_for(std::chrono::duration<double>(diff - syncThreshold));
continue;
}
} else {
drift = clock(t0) - pts;
}
this->m_ipcBridge.write(deviceId, frame);
lastPts = pts;
break;
}
i++;
}
bufferSize = 0; bufferSize = 0;
} }
} while (!std::cin.eof() && !exit); } while (!std::cin.eof() && !exit);
@ -1365,8 +1498,6 @@ int AkVCam::CmdParserPrivate::showControls(const StringMap &flags,
int AkVCam::CmdParserPrivate::readControl(const StringMap &flags, int AkVCam::CmdParserPrivate::readControl(const StringMap &flags,
const StringVector &args) const StringVector &args)
{ {
UNUSED(flags);
if (args.size() < 3) { if (args.size() < 3) {
std::cerr << "Not enough arguments." << std::endl; std::cerr << "Not enough arguments." << std::endl;
@ -1389,9 +1520,9 @@ int AkVCam::CmdParserPrivate::readControl(const StringMap &flags,
std::cout << control.value << std::endl; std::cout << control.value << std::endl;
} else { } else {
if (this->containsFlag(flags, "get-control", "-c")) { if (this->containsFlag(flags, "get-control", "-c")) {
auto typeStr = typeStrMap();
std::cout << control.description << std::endl; std::cout << control.description << std::endl;
} }
if (this->containsFlag(flags, "get-control", "-t")) { if (this->containsFlag(flags, "get-control", "-t")) {
auto typeStr = typeStrMap(); auto typeStr = typeStrMap();
std::cout << typeStr[control.type] << std::endl; std::cout << typeStr[control.type] << std::endl;
@ -1871,6 +2002,146 @@ int AkVCam::CmdParserPrivate::dumpInfo(const AkVCam::StringMap &flags,
return 0; return 0;
} }
int AkVCam::CmdParserPrivate::hacks(const AkVCam::StringMap &flags,
const AkVCam::StringVector &args)
{
UNUSED(flags);
UNUSED(args);
auto hacks = this->m_ipcBridge.hacks();
if (hacks.empty())
return 0;
if (this->m_parseable) {
for (auto &hack: hacks)
std::cout << hack << std::endl;
} else {
std::cout << "Hacks are intended to fix common problems with the "
"virtual camera, and are intended to be used by developers "
"and advanced users only." << std::endl;
std::cout << std::endl;
std::cout << "WARNING: Unsafe hacks can brick your system, make it "
"unstable, or expose it to a serious security risk. You "
"are solely responsible of whatever happens for using "
"them. You been warned, don't come and cry later."
<< std::endl;
std::cout << std::endl;
std::vector<std::string> table {
"Hack",
"Is safe?",
"Description"
};
auto columns = table.size();
for (auto &hack: hacks) {
table.push_back(hack);
table.push_back(this->m_ipcBridge.hackIsSafe(hack)? "Yes": "No");
table.push_back(this->m_ipcBridge.hackDescription(hack));
}
this->drawTable(table, columns);
}
return 0;
}
int AkVCam::CmdParserPrivate::hackInfo(const AkVCam::StringMap &flags,
const AkVCam::StringVector &args)
{
if (args.size() < 2) {
std::cerr << "Not enough arguments." << std::endl;
return -1;
}
auto hack = args[1];
auto hacks = this->m_ipcBridge.hacks();
auto dit = std::find(hacks.begin(), hacks.end(), hack);
if (dit == hacks.end()) {
std::cerr << "Unknown hack: " << hack << "." << std::endl;
return -1;
}
if (this->containsFlag(flags, "hack-info", "-c"))
std::cout << this->m_ipcBridge.hackDescription(hack) << std::endl;
if (this->containsFlag(flags, "hack-info", "-s")) {
if (this->m_ipcBridge.hackIsSafe(hack))
std::cout << "Yes" << std::endl;
else
std::cout << "No" << std::endl;
}
return 0;
}
int AkVCam::CmdParserPrivate::hack(const AkVCam::StringMap &flags,
const AkVCam::StringVector &args)
{
if (args.size() < 2) {
std::cerr << "Not enough arguments." << std::endl;
return -1;
}
auto hack = args[1];
auto hacks = this->m_ipcBridge.hacks();
auto dit = std::find(hacks.begin(), hacks.end(), hack);
if (dit == hacks.end()) {
std::cerr << "Unknown hack: " << hack << "." << std::endl;
return -1;
}
bool accepted = this->m_parseable | this->m_ipcBridge.hackIsSafe(hack);
if (!accepted && !this->m_parseable) {
std::cout << "WARNING: Applying this hack can brick your system, make "
"it unstable, or expose it to a serious security risk. "
"Agreeing to continue, you accept the full responsability "
"of whatever happens from now on."
<< std::endl;
std::cout << std::endl;
if (this->containsFlag(flags, "hack", "-y")) {
std::cout << "You agreed to continue from command line."
<< std::endl;
std::cout << std::endl;
accepted = true;
} else {
std::cout << "If you agree to continue write YES: ";
std::string answer;
std::cin >> answer;
std::cout << std::endl;
accepted = answer == "YES";
}
}
if (!accepted) {
std::cerr << "Hack not applied." << std::endl;
return -1;
}
StringVector hargs;
for (size_t i = 2; i < args.size(); i++)
hargs.push_back(args[i]);
auto result = this->m_ipcBridge.execHack(hack, hargs);
if (result == 0)
std::cout << "Success" << std::endl;
else
std::cout << "Failed" << std::endl;
return result;
}
void AkVCam::CmdParserPrivate::loadGenerals(Settings &settings) void AkVCam::CmdParserPrivate::loadGenerals(Settings &settings)
{ {
settings.beginGroup("General"); settings.beginGroup("General");
@ -2081,3 +2352,22 @@ std::string AkVCam::operator *(size_t n, const std::string &str)
return ss.str(); return ss.str();
} }
AkVCam::CmdParserCommand::CmdParserCommand()
{
}
AkVCam::CmdParserCommand::CmdParserCommand(const std::string &command,
const std::string &arguments,
const std::string &helpString,
const AkVCam::ProgramOptionsFunc &func,
const std::vector<AkVCam::CmdParserFlags> flags,
bool advanced):
command(command),
arguments(arguments),
helpString(helpString),
func(func),
flags(flags),
advanced(advanced)
{
}

View file

@ -42,7 +42,8 @@ namespace AkVCam {
void addCommand(const std::string &command, void addCommand(const std::string &command,
const std::string &arguments, const std::string &arguments,
const std::string &helpString, const std::string &helpString,
const ProgramOptionsFunc &func); const ProgramOptionsFunc &func,
bool advanced=false);
void addFlags(const std::string &command, void addFlags(const std::string &command,
const StringVector &flags, const StringVector &flags,
const std::string &value, const std::string &value,

View file

@ -17,6 +17,7 @@
* Web-Site: http://webcamoid.github.io/ * Web-Site: http://webcamoid.github.io/
*/ */
#include <cmath>
#include <cstdlib> #include <cstdlib>
#include <sstream> #include <sstream>
#include <string> #include <string>
@ -143,6 +144,42 @@ std::string AkVCam::Fraction::toString() const
return ss.str(); return ss.str();
} }
bool AkVCam::Fraction::isInfinity() const
{
return this->d->m_num != 0 && this->d->m_den == 0;
}
int AkVCam::Fraction::sign() const
{
return std::signbit(this->d->m_num) == std::signbit(this->d->m_den)? 1: -1;
}
bool AkVCam::Fraction::isFraction(const std::string &str)
{
auto pos = str.find('/');
if (pos == std::string::npos) {
auto strCpy = trimmed(str);
char *p = nullptr;
strtol(strCpy.c_str(), &p, 10);
if (*p)
return false;
} else {
auto numStr = trimmed(str.substr(0, pos));
auto denStr = trimmed(str.substr(pos + 1));
char *p = nullptr;
char *q = nullptr;
strtol(numStr.c_str(), &p, 10);
strtol(denStr.c_str(), &q, 10);
if (*p || *q)
return false;
}
return true;
}
std::ostream &operator <<(std::ostream &os, const AkVCam::Fraction &fraction) std::ostream &operator <<(std::ostream &os, const AkVCam::Fraction &fraction)
{ {
os << fraction.toString(); os << fraction.toString();

View file

@ -47,6 +47,9 @@ namespace AkVCam
int64_t &den(); int64_t &den();
double value() const; double value() const;
std::string toString() const; std::string toString() const;
bool isInfinity() const;
int sign() const;
static bool isFraction(const std::string &str);
private: private:
FractionPrivate *d; FractionPrivate *d;

View file

@ -174,6 +174,15 @@ namespace AkVCam
bool needsRoot(const std::string &operation) const; bool needsRoot(const std::string &operation) const;
int sudo(int argc, char **argv) const; int sudo(int argc, char **argv) const;
/* Hacks */
std::vector<std::string> hacks() const;
std::string hackDescription(const std::string &hack) const;
bool hackIsSafe(const std::string &hack) const;
bool hackNeedsRoot(const std::string &hack) const;
int execHack(const std::string &hack,
const std::vector<std::string> &args);
private: private:
IpcBridgePrivate *d; IpcBridgePrivate *d;

View file

@ -151,3 +151,11 @@ std::pair<std::string, std::string> AkVCam::splitOnce(const std::string &str,
return {first, second}; return {first, second};
} }
void AkVCam::move(const std::string &from, const std::string &to)
{
std::ifstream infile(from, std::ios::in | std::ios::binary);
std::ofstream outfile(to, std::ios::out | std::ios::binary);
outfile << infile.rdbuf();
std::remove(from.c_str());
}

View file

@ -154,6 +154,7 @@ namespace AkVCam
std::vector<std::string> split(const std::string &str, char separator); std::vector<std::string> split(const std::string &str, char separator);
std::pair<std::string, std::string> splitOnce(const std::string &str, std::pair<std::string, std::string> splitOnce(const std::string &str,
const std::string &separator); const std::string &separator);
void move(const std::string &from, const std::string &to);
} }
#endif // AKVCAMUTILS_UTILS_H #endif // AKVCAMUTILS_UTILS_H

View file

@ -45,8 +45,32 @@
#define AKVCAM_BIND_FUNC(member) \ #define AKVCAM_BIND_FUNC(member) \
std::bind(&member, this, std::placeholders::_1, std::placeholders::_2) std::bind(&member, this, std::placeholders::_1, std::placeholders::_2)
#define AKVCAM_BIND_HACK_FUNC(member) \
std::bind(&member, this, std::placeholders::_1)
namespace AkVCam namespace AkVCam
{ {
class Hack
{
public:
using HackFunc = std::function<int (const std::vector<std::string> &args)>;
std::string name;
std::string description;
bool isSafe {false};
bool needsRoot {false};
HackFunc func;
Hack();
Hack(const std::string &name,
const std::string &description,
bool isSafe,
bool needsRoot,
const HackFunc &func);
Hack(const Hack &other);
Hack &operator =(const Hack &other);
};
class IpcBridgePrivate class IpcBridgePrivate
{ {
public: public:
@ -81,6 +105,22 @@ namespace AkVCam
// Utility methods // Utility methods
bool fileExists(const std::string &path) const; bool fileExists(const std::string &path) const;
static std::string locatePluginPath(); static std::string locatePluginPath();
bool isRoot() const;
std::vector<std::string> listServices() const;
std::vector<std::string> listDisabledServices() const;
inline bool isServiceLoaded(const std::string &service) const;
inline bool isServiceDisabled(const std::string &service) const;
bool readEntitlements(const std::string &app,
const std::string &output) const;
// Hacks
const std::vector<Hack> &hacks();
int setServiceUp(const std::vector<std::string> &args);
int setServiceDown(const std::vector<std::string> &args);
int disableLibraryValidation(const std::vector<std::string> &args);
int codeResign(const std::vector<std::string> &args);
int unsign(const std::vector<std::string> &args);
int disableSIP(const std::vector<std::string> &args);
private: private:
std::vector<IpcBridge *> m_bridges; std::vector<IpcBridge *> m_bridges;
@ -763,15 +803,64 @@ bool AkVCam::IpcBridge::removeListener(const std::string &deviceId)
bool AkVCam::IpcBridge::needsRoot(const std::string &operation) const bool AkVCam::IpcBridge::needsRoot(const std::string &operation) const
{ {
UNUSED(operation); static const std::vector<std::string> operations;
auto it = std::find(operations.begin(), operations.end(), operation);
return false; return it != operations.end() && !this->d->isRoot();
} }
int AkVCam::IpcBridge::sudo(int argc, char **argv) const int AkVCam::IpcBridge::sudo(int argc, char **argv) const
{ {
UNUSED(argc); UNUSED(argc);
UNUSED(argv); UNUSED(argv);
std::cerr << "You must run this command with administrator privileges." << std::endl;
return -1;
}
std::vector<std::string> AkVCam::IpcBridge::hacks() const
{
std::vector<std::string> hacks;
for (auto &hack: this->d->hacks())
hacks.push_back(hack.name);
return hacks;
}
std::string AkVCam::IpcBridge::hackDescription(const std::string &hack) const
{
for (auto &hck: this->d->hacks())
if (hck.name == hack)
return hck.description;
return {};
}
bool AkVCam::IpcBridge::hackIsSafe(const std::string &hack) const
{
for (auto &hck: this->d->hacks())
if (hck.name == hack)
return hck.isSafe;
return true;
}
bool AkVCam::IpcBridge::hackNeedsRoot(const std::string &hack) const
{
for (auto &hck: this->d->hacks())
if (hck.name == hack)
return hck.needsRoot && !this->d->isRoot();
return false;
}
int AkVCam::IpcBridge::execHack(const std::string &hack,
const std::vector<std::string> &args)
{
for (auto &hck: this->d->hacks())
if (hck.name == hack)
return hck.func(args);
return 0; return 0;
} }
@ -1061,3 +1150,470 @@ std::string AkVCam::IpcBridgePrivate::locatePluginPath()
return realPath(dirName + "/../.."); return realPath(dirName + "/../..");
} }
bool AkVCam::IpcBridgePrivate::isRoot() const
{
AkLogFunction();
return getuid() == 0;
}
std::vector<std::string> AkVCam::IpcBridgePrivate::listServices() const
{
std::vector<std::string> services;
auto proc = popen("launchctl list", "r");
if (proc) {
while (!feof(proc)) {
char line[1024];
if (!fgets(line, 1024, proc))
break;
if (strncmp(line, "PID", 3) == 0)
continue;
char *pline = strtok(line, " \n\r\t");
for (size_t i = 0; i < 3 && pline != nullptr; i++) {
if (i == 2)
services.push_back(pline);
pline = strtok(nullptr, " \n\r\t");
}
}
pclose(proc);
}
return services;
}
std::vector<std::string> AkVCam::IpcBridgePrivate::listDisabledServices() const
{
std::vector<std::string> services;
auto proc = popen("launchctl print-disabled system/", "r");
if (proc) {
while (!feof(proc)) {
char line[1024];
if (!fgets(line, 1024, proc))
break;
if (strncmp(line, "\t", 1) != 0)
continue;
std::string service;
char *pline = strtok(line, " ");
for (size_t i = 0; i < 3 && pline; i++) {
if (i == 0)
service = std::string(pline).substr(1, strlen(pline) - 2);
if (i == 2 && strncmp(pline, "true", 4) == 0)
services.push_back(service);
pline = strtok(nullptr, " ");
}
}
pclose(proc);
}
return services;
}
bool AkVCam::IpcBridgePrivate::isServiceLoaded(const std::string &service) const
{
auto services = this->listServices();
return std::find(services.begin(), services.end(), service) != services.end();
}
bool AkVCam::IpcBridgePrivate::isServiceDisabled(const std::string &service) const
{
auto services = this->listDisabledServices();
return std::find(services.begin(), services.end(), service) != services.end();
}
bool AkVCam::IpcBridgePrivate::readEntitlements(const std::string &app,
const std::string &output) const
{
bool writen = false;
std::string cmd = "codesign -d --entitlements - \"" + app + "\"";
auto proc = popen(cmd.c_str(), "r");
if (proc) {
auto entitlements = fopen(output.c_str(), "w");
if (entitlements) {
for (size_t i = 0; !feof(proc); i++) {
char data[1024];
auto len = fread(data, 1, 1024, proc);
if (len < 1)
break;
size_t offset = 0;
if (i == 0)
offset = std::string(data, len).find("<?xml");
fwrite(data + offset, 1, len - offset, entitlements);
writen = true;
}
fclose(entitlements);
}
pclose(proc);
}
return writen;
}
const std::vector<AkVCam::Hack> &AkVCam::IpcBridgePrivate::hacks()
{
static const std::vector<AkVCam::Hack> hacks {
{"set-service-up",
"Setup and start virtual camera service if isn't working",
true,
true,
AKVCAM_BIND_HACK_FUNC(IpcBridgePrivate::setServiceUp)},
{"set-service-down",
"Stop and unregister virtual camera service",
true,
true,
AKVCAM_BIND_HACK_FUNC(IpcBridgePrivate::setServiceDown)},
{"disable-library-validation",
"Disable external plugins validation in app bundle",
false,
false,
AKVCAM_BIND_HACK_FUNC(IpcBridgePrivate::disableLibraryValidation)},
{"code-re-sign",
"Remove app code signature and re-sign it with a developer signature",
false,
false,
AKVCAM_BIND_HACK_FUNC(IpcBridgePrivate::codeResign)},
{"unsign",
"Remove app code signature",
false,
false,
AKVCAM_BIND_HACK_FUNC(IpcBridgePrivate::unsign)},
{"disable-sip",
"Disable System Integrity Protection",
false,
false,
AKVCAM_BIND_HACK_FUNC(IpcBridgePrivate::disableSIP)}
};
return hacks;
}
int AkVCam::IpcBridgePrivate::setServiceUp(const std::vector<std::string> &args)
{
UNUSED(args);
AkLogFunction();
std::string pluginPath = this->locatePluginPath();
static const std::string dstPluginPath =
"/Library/CoreMediaIO/Plug-Ins/DAL/" CMIO_PLUGIN_NAME ".plugin";
if (!fileExists(dstPluginPath))
if (symlink(pluginPath.c_str(), dstPluginPath.c_str()) != 0) {
std::cerr << strerror(errno) << std::endl;
return -1;
}
static const std::string daemonPlist =
"/Library/LaunchDaemons/" CMIO_ASSISTANT_NAME ".plist";
if (!fileExists(daemonPlist)) {
std::ofstream plist(daemonPlist);
if (!plist.is_open())
return -1;
plist << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
plist << "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" ";
plist << "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" << std::endl;
plist << "<plist version=\"1.0\">" << std::endl;
plist << " <dict>" << std::endl;
plist << " <key>Label</key>" << std::endl;
plist << " <string>" CMIO_ASSISTANT_NAME "</string>" << std::endl;
plist << " <key>ProgramArguments</key>" << std::endl;
plist << " <array>" << std::endl;
plist << " <string>";
plist << pluginPath;
plist << "/Contents/Resources/AkVCamAssistant";
plist << "</string>" << std::endl;
plist << " <string>--timeout</string>" << std::endl;
plist << " <string>300.0</string>" << std::endl;
plist << " </array>" << std::endl;
plist << " <key>MachServices</key>" << std::endl;
plist << " <dict>" << std::endl;
plist << " <key>" CMIO_ASSISTANT_NAME "</key>" << std::endl;
plist << " <true/>" << std::endl;
plist << " </dict>" << std::endl;
plist << " <key>StandardOutPath</key>" << std::endl;
plist << " <string>/tmp/AkVCamAssistant.log</string>" << std::endl;
plist << " <key>StandardErrorPath</key>" << std::endl;
plist << " <string>/tmp/AkVCamAssistant.log</string>" << std::endl;
plist << " </dict>" << std::endl;
plist << "</plist>" << std::endl;
plist.close();
}
if (this->isServiceDisabled(CMIO_ASSISTANT_NAME)) {
int result = system("launchctl enable system/" CMIO_ASSISTANT_NAME);
if (result)
return result;
}
if (!this->isServiceLoaded(CMIO_ASSISTANT_NAME)) {
auto cmd = "launchctl bootstrap system " + daemonPlist;
int result = system(cmd.c_str());
if (result)
return result;
}
return 0;
}
int AkVCam::IpcBridgePrivate::setServiceDown(const std::vector<std::string> &args)
{
UNUSED(args);
AkLogFunction();
static const std::string daemonPlist =
"/Library/LaunchDaemons/" CMIO_ASSISTANT_NAME ".plist";
if (fileExists(daemonPlist)) {
if (this->isServiceLoaded(CMIO_ASSISTANT_NAME)) {
auto cmd = "launchctl bootout system " + daemonPlist;
int result = system(cmd.c_str());
if (result)
return result;
}
std::remove(daemonPlist.c_str());
}
static const std::string dstPluginPath =
"/Library/CoreMediaIO/Plug-Ins/DAL/" CMIO_PLUGIN_NAME ".plugin";
if (this->fileExists(dstPluginPath))
if (unlink(dstPluginPath.c_str()) != 0) {
std::cerr << strerror(errno) << std::endl;
return -1;
}
return 0;
}
int AkVCam::IpcBridgePrivate::disableLibraryValidation(const std::vector<std::string> &args)
{
if (args.size() < 1) {
std::cerr << "Not enough arguments." << std::endl;
return -1;
}
if (!this->fileExists(args[0])) {
std::cerr << "No such file or directory." << std::endl;
return -1;
}
static const std::string entitlementsXml = "/tmp/entitlements.xml";
static const std::string dlv =
"com.apple.security.cs.disable-library-validation";
if (this->readEntitlements(args[0], entitlementsXml)) {
auto nsdlv = [NSString
stringWithCString: dlv.c_str()
encoding: [NSString defaultCStringEncoding]];
auto nsEntitlementsXml = [NSString
stringWithCString: entitlementsXml.c_str()
encoding: [NSString defaultCStringEncoding]];
auto nsEntitlementsXmlUrl = [NSURL
fileURLWithPath: nsEntitlementsXml];
NSError *error = nil;
auto entitlements =
[[NSXMLDocument alloc]
initWithContentsOfURL: nsEntitlementsXmlUrl
options: 0
error: &error];
[nsEntitlementsXmlUrl release];
const std::string xpath =
"/plist/dict/key[contains(text(), \"" + dlv + "\")]";
auto nsxpath = [NSString
stringWithCString: xpath.c_str()
encoding: [NSString defaultCStringEncoding]];
auto nodes = [entitlements nodesForXPath: nsxpath error: &error];
[nsxpath release];
if ([nodes count] < 1) {
auto key = [[NSXMLElement alloc] initWithName: @"key" stringValue: nsdlv];
[(NSXMLElement *) entitlements.rootElement.children[0] addChild: key];
[key release];
auto value = [[NSXMLElement alloc] initWithName: @"true"];
[(NSXMLElement *) entitlements.rootElement.children[0] addChild: value];
[value release];
} else {
for (NSXMLNode *node: nodes) {
auto value = [[NSXMLElement alloc] initWithName: @"true"];
[(NSXMLElement *) node.parent replaceChildAtIndex: node.nextSibling.index withNode: value];
[value release];
}
}
auto data = [entitlements XMLDataWithOptions: NSXMLNodePrettyPrint
| NSXMLNodeCompactEmptyElement];
[data writeToFile: nsEntitlementsXml atomically: YES];
[data release];
[entitlements release];
[nsEntitlementsXml release];
[nsdlv release];
} else {
std::ofstream entitlements(entitlementsXml);
if (entitlements.is_open()) {
entitlements << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" << std::endl;
entitlements << "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" << std::endl;
entitlements << "<plist version=\"1.0\">" << std::endl;
entitlements << "\t<dict>" << std::endl;
entitlements << "\t\t<key>" << dlv << "</key>" << std::endl;
entitlements << "\t\t<true/>" << std::endl;
entitlements << "\t</dict>" << std::endl;
entitlements << "</plist>" << std::endl;
entitlements.close();
}
}
auto cmd = "codesign --entitlements \""
+ entitlementsXml
+ "\" -f -s - \""
+ args[0]
+ "\"";
int result = system(cmd.c_str());
std::remove(entitlementsXml.c_str());
return result;
}
int AkVCam::IpcBridgePrivate::codeResign(const std::vector<std::string> &args)
{
if (args.size() < 1) {
std::cerr << "Not enough arguments." << std::endl;
return -1;
}
if (!this->fileExists(args[0])) {
std::cerr << "No such file or directory." << std::endl;
return -1;
}
static const std::string entitlementsXml = "/tmp/entitlements.xml";
std::string cmd;
if (this->readEntitlements(args[0], entitlementsXml))
cmd = "codesign --entitlements \""
+ entitlementsXml
+ "\" -f -s - \""
+ args[0]
+ "\"";
else
cmd = "codesign -f -s - \"" + args[0] + "\"";
int result = system(cmd.c_str());
std::remove(entitlementsXml.c_str());
return result;
}
int AkVCam::IpcBridgePrivate::unsign(const std::vector<std::string> &args)
{
if (args.size() < 1) {
std::cerr << "Not enough arguments." << std::endl;
return -1;
}
if (!this->fileExists(args[0])) {
std::cerr << "No such file or directory." << std::endl;
return -1;
}
auto cmd = "codesign --remove-signature \"" + args[0] + "\"";
return system(cmd.c_str());
}
int AkVCam::IpcBridgePrivate::disableSIP(const std::vector<std::string> &args)
{
std::cerr << "SIP (System Integrity Protection) can't be disbled from "
"inside the system, you must reboot your system and then "
"press and hold Command + R keys on boot to enter to the "
"recovery mode, then go to Utilities > Terminal and run:"
<< std::endl;
std::cerr << std::endl;
std::cerr << "csrutil enable --without fs" << std::endl;
std::cerr << std::endl;
std::cerr << "If that does not works, then run:" << std::endl;
std::cerr << std::endl;
std::cerr << "csrutil disable" << std::endl;
std::cerr << std::endl;
return -1;
}
AkVCam::Hack::Hack()
{
}
AkVCam::Hack::Hack(const std::string &name,
const std::string &description,
bool isSafe,
bool needsRoot,
const Hack::HackFunc &func):
name(name),
description(description),
isSafe(isSafe),
needsRoot(needsRoot),
func(func)
{
}
AkVCam::Hack::Hack(const Hack &other):
name(other.name),
description(other.description),
isSafe(other.isSafe),
needsRoot(other.needsRoot),
func(other.func)
{
}
AkVCam::Hack &AkVCam::Hack::operator =(const Hack &other)
{
if (this != &other) {
this->name = other.name;
this->description = other.description;
this->isSafe = other.isSafe;
this->needsRoot = other.needsRoot;
this->func = other.func;
}
return *this;
}

View file

@ -49,6 +49,27 @@ namespace AkVCam
Mutex mutex; Mutex mutex;
}; };
class Hack
{
public:
using HackFunc = std::function<int (const std::vector<std::string> &args)>;
std::string name;
std::string description;
bool isSafe {false};
bool needsRoot {false};
HackFunc func;
Hack();
Hack(const std::string &name,
const std::string &description,
bool isSafe,
bool needsRoot,
const HackFunc &func);
Hack(const Hack &other);
Hack &operator =(const Hack &other);
};
class IpcBridgePrivate class IpcBridgePrivate
{ {
public: public:
@ -87,9 +108,16 @@ namespace AkVCam
int sudo(const std::vector<std::string> &parameters, int sudo(const std::vector<std::string> &parameters,
const std::string &directory={}, const std::string &directory={},
bool show=false); bool show=false);
std::string assistant() const;
std::string manager() const; std::string manager() const;
std::string alternativeManager() const; std::string alternativeManager() const;
std::string alternativePlugin() const; std::string alternativePlugin() const;
std::string service() const;
// Hacks
const std::vector<Hack> &hacks();
int setServiceUp(const std::vector<std::string> &args);
int setServiceDown(const std::vector<std::string> &args);
}; };
static const int maxFrameWidth = 1920; static const int maxFrameWidth = 1920;
@ -826,15 +854,15 @@ bool AkVCam::IpcBridge::needsRoot(const std::string &operation) const
{ {
static const std::vector<std::string> operations { static const std::vector<std::string> operations {
"add-device", "add-device",
"add-format",
"load",
"remove-device", "remove-device",
"remove-devices", "remove-devices",
"set-description",
"add-format",
"remove-format", "remove-format",
"remove-formats", "remove-formats",
"update", "set-description",
"load",
"set-loglevel", "set-loglevel",
"update"
}; };
auto it = std::find(operations.begin(), operations.end(), operation); auto it = std::find(operations.begin(), operations.end(), operation);
@ -853,6 +881,53 @@ int AkVCam::IpcBridge::sudo(int argc, char **argv) const
return this->d->sudo(arguments); return this->d->sudo(arguments);
} }
std::vector<std::string> AkVCam::IpcBridge::hacks() const
{
std::vector<std::string> hacks;
for (auto &hack: this->d->hacks())
hacks.push_back(hack.name);
return hacks;
}
std::string AkVCam::IpcBridge::hackDescription(const std::string &hack) const
{
for (auto &hck: this->d->hacks())
if (hck.name == hack)
return hck.description;
return {};
}
bool AkVCam::IpcBridge::hackIsSafe(const std::string &hack) const
{
for (auto &hck: this->d->hacks())
if (hck.name == hack)
return hck.isSafe;
return true;
}
bool AkVCam::IpcBridge::hackNeedsRoot(const std::string &hack) const
{
for (auto &hck: this->d->hacks())
if (hck.name == hack)
return hck.needsRoot && !this->d->isRoot();
return false;
}
int AkVCam::IpcBridge::execHack(const std::string &hack,
const std::vector<std::string> &args)
{
for (auto &hck: this->d->hacks())
if (hck.name == hack)
return hck.func(args);
return 0;
}
AkVCam::IpcBridgePrivate::IpcBridgePrivate(IpcBridge *self): AkVCam::IpcBridgePrivate::IpcBridgePrivate(IpcBridge *self):
self(self) self(self)
{ {
@ -1180,6 +1255,31 @@ int AkVCam::IpcBridgePrivate::sudo(const std::vector<std::string> &parameters,
return int(exitCode); return int(exitCode);
} }
std::string AkVCam::IpcBridgePrivate::assistant() const
{
AkLogFunction();
auto pluginPath = locatePluginPath();
auto path = realPath(pluginPath + "\\" DSHOW_PLUGIN_ASSISTANT_NAME ".exe");
if (fileExists(path))
return path;
#ifdef _WIN64
path = realPath(pluginPath + "\\..\\x86\\" DSHOW_PLUGIN_ASSISTANT_NAME ".exe");
#else
SYSTEM_INFO info;
memset(&info, 0, sizeof(SYSTEM_INFO));
GetNativeSystemInfo(&info);
if (info.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_INTEL)
return {};
path = realPath(pluginPath + "\\..\\x64\\" DSHOW_PLUGIN_ASSISTANT_NAME ".exe");
#endif
return fileExists(path)? path: std::string();
}
std::string AkVCam::IpcBridgePrivate::manager() const std::string AkVCam::IpcBridgePrivate::manager() const
{ {
AkLogFunction(); AkLogFunction();
@ -1230,3 +1330,188 @@ std::string AkVCam::IpcBridgePrivate::alternativePlugin() const
return fileExists(path)? path: std::string(); return fileExists(path)? path: std::string();
} }
std::string AkVCam::IpcBridgePrivate::service() const
{
std::string path;
auto manager = OpenSCManager(nullptr, nullptr, GENERIC_READ);
if (manager) {
auto service = OpenServiceA(manager,
DSHOW_PLUGIN_ASSISTANT_NAME,
SERVICE_QUERY_CONFIG);
if (service) {
DWORD bytesNeeded = 0;
QueryServiceConfig(service, nullptr, 0, &bytesNeeded);
auto bufSize = bytesNeeded;
auto serviceConfig =
reinterpret_cast<LPQUERY_SERVICE_CONFIGA>(LocalAlloc(LMEM_FIXED,
bufSize));
if (serviceConfig) {
if (QueryServiceConfigA(service,
serviceConfig,
bufSize,
&bytesNeeded)) {
path = std::string(serviceConfig->lpBinaryPathName);
}
LocalFree(serviceConfig);
}
CloseServiceHandle(service);
}
CloseServiceHandle(manager);
}
return path;
}
const std::vector<AkVCam::Hack> &AkVCam::IpcBridgePrivate::hacks()
{
static const std::vector<AkVCam::Hack> hacks {
{"set-service-up",
"Setup and start virtual camera service if isn't working",
true,
true,
AKVCAM_BIND_FUNC(IpcBridgePrivate::setServiceUp)},
{"set-service-down",
"Stop and unregister virtual camera service",
true,
true,
AKVCAM_BIND_FUNC(IpcBridgePrivate::setServiceDown)}
};
return hacks;
}
int AkVCam::IpcBridgePrivate::setServiceUp(const std::vector<std::string> &args)
{
UNUSED(args);
AkLogFunction();
// If the service is not installed, install it.
auto servicePath = this->service();
if (servicePath.empty()) {
auto assistant = this->assistant();
if (assistant.empty())
return -1;
auto result = this->sudo({assistant, "--install"});
if (result < 0)
return result;
}
// Start the service.
bool result = false;
auto manager = OpenSCManager(nullptr, nullptr, SC_MANAGER_CONNECT);
if (manager) {
auto service = OpenService(manager,
TEXT(DSHOW_PLUGIN_ASSISTANT_NAME),
SERVICE_START);
if (service) {
result = StartService(service, 0, nullptr);
CloseServiceHandle(service);
}
CloseServiceHandle(manager);
}
return result? 0: -1;
}
int AkVCam::IpcBridgePrivate::setServiceDown(const std::vector<std::string> &args)
{
UNUSED(args);
AkLogFunction();
auto servicePath = this->service();
if (servicePath.empty())
return 0;
// Stop the service.
bool stopped = false;
auto manager = OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
if (manager) {
auto service = OpenService(manager,
TEXT(DSHOW_PLUGIN_ASSISTANT_NAME),
SERVICE_STOP | SERVICE_QUERY_STATUS);
if (service) {
SERVICE_STATUS status;
memset(&status, 0, sizeof(SERVICE_STATUS));
if (ControlService(service, SERVICE_CONTROL_STOP, &status)) {
memset(&status, 0, sizeof(SERVICE_STATUS));
while(QueryServiceStatus(service, &status)) {
if (status.dwCurrentState != SERVICE_STOP_PENDING)
break;
Sleep(1000);
}
stopped = status.dwCurrentState == SERVICE_STOPPED;
}
CloseServiceHandle(service);
}
CloseServiceHandle(manager);
}
if (!stopped)
return -1;
// Unistall the service.
return this->sudo({servicePath, "--uninstall"});
}
AkVCam::Hack::Hack()
{
}
AkVCam::Hack::Hack(const std::string &name,
const std::string &description,
bool isSafe,
bool needsRoot,
const Hack::HackFunc &func):
name(name),
description(description),
isSafe(isSafe),
needsRoot(needsRoot),
func(func)
{
}
AkVCam::Hack::Hack(const Hack &other):
name(other.name),
description(other.description),
isSafe(other.isSafe),
needsRoot(other.needsRoot),
func(other.func)
{
}
AkVCam::Hack &AkVCam::Hack::operator =(const Hack &other)
{
if (this != &other) {
this->name = other.name;
this->description = other.description;
this->isSafe = other.isSafe;
this->needsRoot = other.needsRoot;
this->func = other.func;
}
return *this;
}

View file

@ -203,7 +203,9 @@ HRESULT AkVCam::Pin::stateChanged(void *userData, FILTER_STATE state)
return VFW_E_NOT_COMMITTED; return VFW_E_NOT_COMMITTED;
self->d->updateTestFrame(); self->d->updateTestFrame();
self->d->m_mutex.lock();
self->d->m_currentFrame = self->d->m_testFrameAdapted; self->d->m_currentFrame = self->d->m_testFrameAdapted;
self->d->m_mutex.unlock();
self->d->m_pts = -1; self->d->m_pts = -1;
self->d->m_ptsDrift = 0; self->d->m_ptsDrift = 0;
@ -241,7 +243,9 @@ HRESULT AkVCam::Pin::stateChanged(void *userData, FILTER_STATE state)
CloseHandle(self->d->m_sendFrameEvent); CloseHandle(self->d->m_sendFrameEvent);
self->d->m_sendFrameEvent = nullptr; self->d->m_sendFrameEvent = nullptr;
self->d->m_memAllocator->Decommit(); self->d->m_memAllocator->Decommit();
self->d->m_mutex.lock();
self->d->m_currentFrame.clear(); self->d->m_currentFrame.clear();
self->d->m_mutex.unlock();
self->d->m_testFrameAdapted.clear(); self->d->m_testFrameAdapted.clear();
} }

0
ports/ci/appveyor/build.bat Executable file → Normal file
View file

0
ports/ci/appveyor/deploy.bat Executable file → Normal file
View file

0
ports/ci/appveyor/install_deps.bat Executable file → Normal file
View file

0
ports/ci/appveyor/push_artifacts.bat Executable file → Normal file
View file

0
ports/ci/appveyor/upload.bat Executable file → Normal file
View file