Fix #345
This commit is contained in:
@@ -514,44 +514,6 @@ namespace confighttp {
|
||||
std::string content = file_handler::read_file(config::stream.file_apps.c_str());
|
||||
nlohmann::json file_tree = nlohmann::json::parse(content);
|
||||
|
||||
// Legacy versions of Sunshine/Apollo used strings for boolean and integers, let's convert them
|
||||
// List of keys to convert to boolean
|
||||
std::vector<std::string> boolean_keys = {
|
||||
"exclude-global-prep-cmd",
|
||||
"elevated",
|
||||
"auto-detach",
|
||||
"wait-all",
|
||||
"use-app-identity",
|
||||
"per-client-app-identity",
|
||||
"virtual-display"
|
||||
};
|
||||
|
||||
// List of keys to convert to integers
|
||||
std::vector<std::string> integer_keys = {
|
||||
"exit-timeout"
|
||||
};
|
||||
|
||||
// Walk fileTree and convert true/false strings to boolean or integer values
|
||||
for (auto &app : file_tree["apps"]) {
|
||||
for (const auto &key : boolean_keys) {
|
||||
if (app.contains(key) && app[key].is_string()) {
|
||||
app[key] = app[key] == "true";
|
||||
}
|
||||
}
|
||||
for (const auto &key : integer_keys) {
|
||||
if (app.contains(key) && app[key].is_string()) {
|
||||
app[key] = std::stoi(app[key].get<std::string>());
|
||||
}
|
||||
}
|
||||
if (app.contains("prep-cmd")) {
|
||||
for (auto &prep : app["prep-cmd"]) {
|
||||
if (prep.contains("elevated") && prep["elevated"].is_string()) {
|
||||
prep["elevated"] = prep["elevated"] == "true";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file_tree["current_app"] = proc::proc.get_running_app_uuid();
|
||||
|
||||
send_response(response, file_tree);
|
||||
@@ -562,7 +524,8 @@ namespace confighttp {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Save an application. To save a new application the index must be `-1`. To update an existing application, you must provide the current index of the application.
|
||||
* @brief Save an application. To save a new application the UUID must be empty.
|
||||
* To update an existing application, you must provide the current UUID of the application.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
* The body for the post request should be JSON serialized in the following format:
|
||||
@@ -571,7 +534,6 @@ namespace confighttp {
|
||||
* "name": "Application Name",
|
||||
* "output": "Log Output Path",
|
||||
* "cmd": "Command to run the application",
|
||||
* "index": -1,
|
||||
* "exclude-global-prep-cmd": false,
|
||||
* "elevated": false,
|
||||
* "auto-detach": true,
|
||||
@@ -587,11 +549,12 @@ namespace confighttp {
|
||||
* "detached": [
|
||||
* "Detached command"
|
||||
* ],
|
||||
* "image-path": "Full path to the application image. Must be a png file."
|
||||
* "image-path": "Full path to the application image. Must be a png file.",
|
||||
* "uuid": "aaaa-bbbb"
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @api_examples{/api/apps| POST| {"name":"Hello, World!","index":-1}}
|
||||
* @api_examples{/api/apps| POST| {"name":"Hello, World!","uuid": "aaaa-bbbb"}}
|
||||
*/
|
||||
void saveApp(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) {
|
||||
@@ -602,39 +565,31 @@ namespace confighttp {
|
||||
|
||||
std::stringstream ss;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
BOOST_LOG(info) << config::stream.file_apps;
|
||||
try {
|
||||
nlohmann::json input_tree = nlohmann::json::parse(ss.str());
|
||||
std::string file = file_handler::read_file(config::stream.file_apps.c_str());
|
||||
nlohmann::json file_tree = nlohmann::json::parse(file);
|
||||
// Remove empty keys if needed
|
||||
if (input_tree.contains("prep-cmd") && input_tree["prep-cmd"].empty())
|
||||
input_tree.erase("prep-cmd");
|
||||
if (input_tree.contains("detached") && input_tree["detached"].empty())
|
||||
input_tree.erase("detached");
|
||||
auto &apps_node = file_tree["apps"];
|
||||
int index = input_tree["index"].get<int>();
|
||||
input_tree.erase("index");
|
||||
if (index == -1) {
|
||||
apps_node.push_back(input_tree);
|
||||
} else {
|
||||
nlohmann::json newApps = nlohmann::json::array();
|
||||
for (size_t i = 0; i < apps_node.size(); ++i) {
|
||||
if (static_cast<int>(i) == index)
|
||||
newApps.push_back(input_tree);
|
||||
else
|
||||
newApps.push_back(apps_node[i]);
|
||||
}
|
||||
file_tree["apps"] = newApps;
|
||||
}
|
||||
std::sort(apps_node.begin(), apps_node.end(), [](const nlohmann::json &a, const nlohmann::json &b) {
|
||||
return a["name"].get<std::string>() < b["name"].get<std::string>();
|
||||
});
|
||||
file_handler::write_file(config::stream.file_apps.c_str(), file_tree.dump(4));
|
||||
// TODO: Input Validation
|
||||
|
||||
// Read the input JSON from the request body.
|
||||
nlohmann::json inputTree = nlohmann::json::parse(ss.str());
|
||||
|
||||
// Read the existing apps file.
|
||||
std::string content = file_handler::read_file(config::stream.file_apps.c_str());
|
||||
nlohmann::json fileTree = nlohmann::json::parse(content);
|
||||
|
||||
// Migrate/merge the new app into the file tree.
|
||||
proc::migrate_apps(&fileTree, &inputTree);
|
||||
|
||||
// Write the updated file tree back to disk.
|
||||
file_handler::write_file(config::stream.file_apps.c_str(), fileTree.dump(4));
|
||||
proc::refresh(config::stream.file_apps);
|
||||
nlohmann::json output_tree;
|
||||
output_tree["status"] = true;
|
||||
send_response(response, output_tree);
|
||||
} catch (std::exception &e) {
|
||||
|
||||
// Prepare and send the output response.
|
||||
nlohmann::json outputTree;
|
||||
outputTree["status"] = true;
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
|
||||
bad_request(response, request, e.what());
|
||||
}
|
||||
@@ -668,41 +623,44 @@ namespace confighttp {
|
||||
* @api_examples{/api/apps/9999| DELETE| null}
|
||||
*/
|
||||
void deleteApp(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) {
|
||||
if (!authenticate(response, request))
|
||||
return;
|
||||
}
|
||||
|
||||
print_req(request);
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
if (args.find("uuid"s) == std::end(args)) {
|
||||
bad_request(response, request, "Missing a required parameter to delete app");
|
||||
return;
|
||||
}
|
||||
auto uuid = nvhttp::get_arg(args, "uuid");
|
||||
|
||||
try {
|
||||
nlohmann::json output_tree;
|
||||
nlohmann::json new_apps = nlohmann::json::array();
|
||||
std::string file = file_handler::read_file(config::stream.file_apps.c_str());
|
||||
nlohmann::json file_tree = nlohmann::json::parse(file);
|
||||
auto &apps_node = file_tree["apps"];
|
||||
// In this merged version we assume the app index is part of the URL match (convert as needed)
|
||||
const int index = std::stoi(request->path_match[1]);
|
||||
if (index < 0 || index >= static_cast<int>(apps_node.size())) {
|
||||
std::string error;
|
||||
if (apps_node.empty()) {
|
||||
error = "No applications to delete";
|
||||
} else {
|
||||
error = "'index' out of range, max index is " + std::to_string(apps_node.size() - 1);
|
||||
}
|
||||
bad_request(response, request, error);
|
||||
return;
|
||||
// Read the apps file into a nlohmann::json object.
|
||||
std::string content = file_handler::read_file(config::stream.file_apps.c_str());
|
||||
nlohmann::json fileTree = nlohmann::json::parse(content);
|
||||
|
||||
// Remove any app with the matching uuid directly from the "apps" array.
|
||||
if (fileTree.contains("apps") && fileTree["apps"].is_array()) {
|
||||
auto& apps = fileTree["apps"];
|
||||
apps.erase(
|
||||
std::remove_if(apps.begin(), apps.end(), [&uuid](const nlohmann::json& app) {
|
||||
return app.value("uuid", "") == uuid;
|
||||
}),
|
||||
apps.end()
|
||||
);
|
||||
}
|
||||
for (size_t i = 0; i < apps_node.size(); ++i) {
|
||||
if (static_cast<int>(i) != index)
|
||||
new_apps.push_back(apps_node[i]);
|
||||
}
|
||||
file_tree["apps"] = new_apps;
|
||||
file_handler::write_file(config::stream.file_apps.c_str(), file_tree.dump(4));
|
||||
|
||||
// Write the updated JSON back to the file.
|
||||
file_handler::write_file(config::stream.file_apps.c_str(), fileTree.dump(4));
|
||||
proc::refresh(config::stream.file_apps);
|
||||
output_tree["status"] = true;
|
||||
output_tree["result"] = "application " + std::to_string(index) + " deleted";
|
||||
send_response(response, output_tree);
|
||||
} catch (std::exception &e) {
|
||||
|
||||
// Prepare and send the response.
|
||||
nlohmann::json outputTree;
|
||||
outputTree["status"] = true;
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "DeleteApp: "sv << e.what();
|
||||
bad_request(response, request, e.what());
|
||||
}
|
||||
|
||||
539
src/process.cpp
539
src/process.cpp
@@ -24,6 +24,7 @@
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "display_device.h"
|
||||
#include "file_handler.h"
|
||||
#include "logging.h"
|
||||
#include "platform/common.h"
|
||||
#include "process.h"
|
||||
@@ -838,158 +839,280 @@ namespace proc {
|
||||
return std::make_tuple(id_no_index, id_with_index);
|
||||
}
|
||||
|
||||
void migrate_apps(pt::ptree* fileTree_p, pt::ptree* inputTree_p) {
|
||||
/**
|
||||
* @brief Migrate the applications stored in the file tree by merging in a new app.
|
||||
*
|
||||
* This function updates the application entries in *fileTree_p* using the data in *inputTree_p*.
|
||||
* If an app in the file tree does not have a UUID, one is generated and inserted.
|
||||
* If an app with the same UUID as the new app is found, it is replaced.
|
||||
* Additionally, empty keys (such as "prep-cmd" or "detached") and keys no longer needed ("launching", "index")
|
||||
* are removed from the input.
|
||||
*
|
||||
* Legacy versions of Sunshine/Apollo stored boolean and integer values as strings.
|
||||
* The following keys are converted:
|
||||
* - Boolean keys: "exclude-global-prep-cmd", "elevated", "auto-detach", "wait-all",
|
||||
* "use-app-identity", "per-client-app-identity", "virtual-display"
|
||||
* - Integer keys: "exit-timeout"
|
||||
*
|
||||
* A migration version is stored in the file tree (under "version") so that future changes can be applied.
|
||||
*
|
||||
* @param fileTree_p Pointer to the JSON object representing the file tree.
|
||||
* @param inputTree_p Pointer to the JSON object representing the new app.
|
||||
*/
|
||||
void migrate_apps(nlohmann::json* fileTree_p, nlohmann::json* inputTree_p) {
|
||||
std::string new_app_uuid;
|
||||
|
||||
if (inputTree_p) {
|
||||
auto input_uuid = inputTree_p->get_optional<std::string>("uuid"s);
|
||||
if (input_uuid && !input_uuid.value().empty()) {
|
||||
new_app_uuid = input_uuid.value();
|
||||
// If the input contains a non-empty "uuid", use it; otherwise generate one.
|
||||
if (inputTree_p->contains("uuid") && !(*inputTree_p)["uuid"].get<std::string>().empty()) {
|
||||
new_app_uuid = (*inputTree_p)["uuid"].get<std::string>();
|
||||
} else {
|
||||
new_app_uuid = uuid_util::uuid_t::generate().string();
|
||||
inputTree_p->erase("uuid");
|
||||
inputTree_p->put("uuid", new_app_uuid);
|
||||
(*inputTree_p)["uuid"] = new_app_uuid;
|
||||
}
|
||||
|
||||
if (inputTree_p->get_child("prep-cmd").empty()) {
|
||||
// Remove "prep-cmd" if empty.
|
||||
if (inputTree_p->contains("prep-cmd") && (*inputTree_p)["prep-cmd"].empty()) {
|
||||
inputTree_p->erase("prep-cmd");
|
||||
}
|
||||
|
||||
if (inputTree_p->get_child("detached").empty()) {
|
||||
// Remove "detached" if empty.
|
||||
if (inputTree_p->contains("detached") && (*inputTree_p)["detached"].empty()) {
|
||||
inputTree_p->erase("detached");
|
||||
}
|
||||
|
||||
// Remove keys that are no longer needed.
|
||||
inputTree_p->erase("launching");
|
||||
inputTree_p->erase("index");
|
||||
}
|
||||
|
||||
auto &apps_node = fileTree_p->get_child("apps"s);
|
||||
|
||||
pt::ptree newApps;
|
||||
for (auto &kv : apps_node) {
|
||||
// Check if we have apps that have not got an uuid assigned
|
||||
auto app_uuid = kv.second.get_optional<std::string>("uuid"s);
|
||||
if (!app_uuid || app_uuid.value().empty()) {
|
||||
kv.second.erase("uuid");
|
||||
kv.second.put("uuid", uuid_util::uuid_t::generate().string());
|
||||
kv.second.erase("launching");
|
||||
newApps.push_back(std::make_pair("", std::move(kv.second)));
|
||||
} else {
|
||||
if (!new_app_uuid.empty() && app_uuid.value() == new_app_uuid) {
|
||||
newApps.push_back(std::make_pair("", *inputTree_p));
|
||||
new_app_uuid.clear();
|
||||
// Get the current apps array; if it doesn't exist, create one.
|
||||
nlohmann::json newApps = nlohmann::json::array();
|
||||
if (fileTree_p->contains("apps") && (*fileTree_p)["apps"].is_array()) {
|
||||
for (auto &app : (*fileTree_p)["apps"]) {
|
||||
// For apps without a UUID, generate one and remove "launching".
|
||||
if (!app.contains("uuid") || app["uuid"].get<std::string>().empty()) {
|
||||
app["uuid"] = uuid_util::uuid_t::generate().string();
|
||||
app.erase("launching");
|
||||
newApps.push_back(std::move(app));
|
||||
} else {
|
||||
newApps.push_back(std::make_pair("", std::move(kv.second)));
|
||||
// If an app with the same UUID as the new app is found, replace it.
|
||||
if (!new_app_uuid.empty() && app["uuid"].get<std::string>() == new_app_uuid) {
|
||||
newApps.push_back(*inputTree_p);
|
||||
new_app_uuid.clear();
|
||||
} else {
|
||||
newApps.push_back(std::move(app));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the new app's UUID has not been merged yet, add it.
|
||||
if (!new_app_uuid.empty() && inputTree_p) {
|
||||
newApps.push_back(*inputTree_p);
|
||||
}
|
||||
(*fileTree_p)["apps"] = newApps;
|
||||
}
|
||||
|
||||
// Finally add the new app
|
||||
if (!new_app_uuid.empty()) {
|
||||
newApps.push_back(std::make_pair("", *inputTree_p));
|
||||
void migration_v2(nlohmann::json& fileTree) {
|
||||
int this_version = 2;
|
||||
// Determine the current migration version (default to 1 if not present).
|
||||
int file_version = 1;
|
||||
if (fileTree.contains("version")) {
|
||||
file_version = fileTree["version"].get<int>();
|
||||
}
|
||||
|
||||
fileTree_p->erase("apps");
|
||||
fileTree_p->push_back(std::make_pair("apps", newApps));
|
||||
// If the version is less than this_version, perform legacy conversion.
|
||||
if (file_version < this_version) {
|
||||
BOOST_LOG(info) << "Migrating app list from v1 to v2...";
|
||||
migrate_apps(&fileTree, nullptr);
|
||||
|
||||
// List of keys to convert to booleans.
|
||||
std::vector<std::string> boolean_keys = {
|
||||
"allow-client-commands",
|
||||
"exclude-global-prep-cmd",
|
||||
"elevated",
|
||||
"auto-detach",
|
||||
"wait-all",
|
||||
"use-app-identity",
|
||||
"per-client-app-identity",
|
||||
"virtual-display"
|
||||
};
|
||||
|
||||
// List of keys to convert to integers.
|
||||
std::vector<std::string> integer_keys = {
|
||||
"exit-timeout",
|
||||
"scale-factor"
|
||||
};
|
||||
|
||||
// Walk through each app and convert legacy string values.
|
||||
for (auto &app : fileTree["apps"]) {
|
||||
for (const auto &key : boolean_keys) {
|
||||
if (app.contains(key) && app[key].is_string()) {
|
||||
std::string s = app[key].get<std::string>();
|
||||
app[key] = (s == "true");
|
||||
}
|
||||
}
|
||||
for (const auto &key : integer_keys) {
|
||||
if (app.contains(key) && app[key].is_string()) {
|
||||
std::string s = app[key].get<std::string>();
|
||||
app[key] = std::stoi(s);
|
||||
}
|
||||
}
|
||||
// For each entry in the "prep-cmd" array, convert "elevated" if necessary.
|
||||
if (app.contains("prep-cmd") && app["prep-cmd"].is_array()) {
|
||||
for (auto &prep : app["prep-cmd"]) {
|
||||
if (prep.contains("elevated") && prep["elevated"].is_string()) {
|
||||
std::string s = prep["elevated"].get<std::string>();
|
||||
prep["elevated"] = (s == "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update migration version to this_version.
|
||||
fileTree["version"] = this_version;
|
||||
|
||||
BOOST_LOG(info) << "Migrated app list from v1 to v2.";
|
||||
}
|
||||
}
|
||||
|
||||
void migrate(nlohmann::json& fileTree, const std::string& fileName) {
|
||||
int last_version = 2;
|
||||
|
||||
int file_version = 0;
|
||||
if (fileTree.contains("version")) {
|
||||
file_version = fileTree["version"].get<int>();
|
||||
}
|
||||
|
||||
if (file_version < last_version) {
|
||||
migration_v2(fileTree);
|
||||
file_handler::write_file(fileName.c_str(), fileTree.dump(4));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<proc::proc_t> parse(const std::string &file_name) {
|
||||
pt::ptree tree;
|
||||
// Prepare environment variables.
|
||||
auto this_env = boost::this_process::environment();
|
||||
|
||||
std::set<std::string> ids;
|
||||
std::vector<proc::ctx_t> apps;
|
||||
int i = 0;
|
||||
|
||||
try {
|
||||
pt::read_json(file_name, tree);
|
||||
// Read the JSON file into a tree.
|
||||
std::string content = file_handler::read_file(file_name.c_str());
|
||||
nlohmann::json tree = nlohmann::json::parse(content);
|
||||
|
||||
auto &apps_node = tree.get_child("apps"s);
|
||||
auto &env_vars = tree.get_child("env"s);
|
||||
migrate(tree, file_name);
|
||||
|
||||
auto this_env = boost::this_process::environment();
|
||||
|
||||
for (auto &[name, val] : env_vars) {
|
||||
this_env[name] = parse_env_val(this_env, val.get_value<std::string>());
|
||||
}
|
||||
|
||||
std::set<std::string> ids;
|
||||
std::vector<proc::ctx_t> apps;
|
||||
int i = 0;
|
||||
|
||||
if (config::input.enable_input_only_mode) {
|
||||
// Input Only entry
|
||||
{
|
||||
proc::ctx_t ctx;
|
||||
// ctx.uuid = ""; // We're not using uuid for this special entry
|
||||
ctx.name = "Remote Input";
|
||||
ctx.image_path = parse_env_val(this_env, "input_only.png");
|
||||
ctx.virtual_display = false;
|
||||
ctx.scale_factor = 100;
|
||||
ctx.use_app_identity = false;
|
||||
ctx.per_client_app_identity = false;
|
||||
ctx.allow_client_commands = false;
|
||||
|
||||
ctx.elevated = false;
|
||||
ctx.auto_detach = true;
|
||||
ctx.wait_all = true;
|
||||
ctx.exit_timeout = 5s;
|
||||
|
||||
auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++);
|
||||
if (ids.count(std::get<0>(possible_ids)) == 0) {
|
||||
// Avoid using index to generate id if possible
|
||||
ctx.id = std::get<0>(possible_ids);
|
||||
}
|
||||
else {
|
||||
// Fallback to include index on collision
|
||||
ctx.id = std::get<1>(possible_ids);
|
||||
}
|
||||
ids.insert(ctx.id);
|
||||
|
||||
input_only_app_id_str = ctx.id;
|
||||
input_only_app_id = util::from_view(ctx.id);
|
||||
|
||||
apps.emplace_back(std::move(ctx));
|
||||
}
|
||||
|
||||
// Terminate entry
|
||||
{
|
||||
proc::ctx_t ctx;
|
||||
// ctx.uuid = ""; // We're not using uuid for this special entry
|
||||
ctx.name = "Terminate";
|
||||
ctx.image_path = parse_env_val(this_env, "terminate.png");
|
||||
ctx.virtual_display = false;
|
||||
ctx.scale_factor = 100;
|
||||
ctx.use_app_identity = false;
|
||||
ctx.per_client_app_identity = false;
|
||||
ctx.allow_client_commands = false;
|
||||
|
||||
ctx.elevated = false;
|
||||
ctx.auto_detach = true;
|
||||
ctx.wait_all = true;
|
||||
ctx.exit_timeout = 5s;
|
||||
|
||||
auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++);
|
||||
if (ids.count(std::get<0>(possible_ids)) == 0) {
|
||||
// Avoid using index to generate id if possible
|
||||
ctx.id = std::get<0>(possible_ids);
|
||||
}
|
||||
else {
|
||||
// Fallback to include index on collision
|
||||
ctx.id = std::get<1>(possible_ids);
|
||||
}
|
||||
ids.insert(ctx.id);
|
||||
|
||||
terminate_app_id_str = ctx.id;
|
||||
terminate_app_id = util::from_view(ctx.id);
|
||||
|
||||
apps.emplace_back(std::move(ctx));
|
||||
if (tree.contains("env") && tree["env"].is_object()) {
|
||||
for (auto &item : tree["env"].items()) {
|
||||
this_env[item.key()] = parse_env_val(this_env, item.value().get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual Display entry
|
||||
#ifdef _WIN32
|
||||
if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) {
|
||||
// Ensure the "apps" array exists.
|
||||
if (!tree.contains("apps") || !tree["apps"].is_array()) {
|
||||
BOOST_LOG(warning) << "No apps were defined in apps.json!!!";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Iterate over each application in the "apps" array.
|
||||
for (auto &app_node : tree["apps"]) {
|
||||
proc::ctx_t ctx;
|
||||
ctx.uuid = app_node.value("uuid", "");
|
||||
|
||||
// Build the list of preparation commands.
|
||||
std::vector<proc::cmd_t> prep_cmds;
|
||||
bool exclude_global_prep = app_node.value("exclude-global-prep-cmd", false);
|
||||
if (!exclude_global_prep) {
|
||||
prep_cmds.reserve(config::sunshine.prep_cmds.size());
|
||||
for (auto &prep_cmd : config::sunshine.prep_cmds) {
|
||||
auto do_cmd = parse_env_val(this_env, prep_cmd.do_cmd);
|
||||
auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_cmd);
|
||||
prep_cmds.emplace_back(
|
||||
std::move(do_cmd),
|
||||
std::move(undo_cmd),
|
||||
std::move(prep_cmd.elevated)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (app_node.contains("prep-cmd") && app_node["prep-cmd"].is_array()) {
|
||||
for (auto &prep_node : app_node["prep-cmd"]) {
|
||||
std::string do_cmd = parse_env_val(this_env, prep_node.value("do", ""));
|
||||
std::string undo_cmd = parse_env_val(this_env, prep_node.value("undo", ""));
|
||||
bool elevated = prep_node.value("elevated", false);
|
||||
prep_cmds.emplace_back(
|
||||
std::move(do_cmd),
|
||||
std::move(undo_cmd),
|
||||
std::move(elevated)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Build the list of detached commands.
|
||||
std::vector<std::string> detached;
|
||||
if (app_node.contains("detached") && app_node["detached"].is_array()) {
|
||||
for (auto &detached_val : app_node["detached"]) {
|
||||
detached.emplace_back(parse_env_val(this_env, detached_val.get<std::string>()));
|
||||
}
|
||||
}
|
||||
|
||||
// Process other fields.
|
||||
if (app_node.contains("output"))
|
||||
ctx.output = parse_env_val(this_env, app_node.value("output", ""));
|
||||
std::string name = parse_env_val(this_env, app_node.value("name", ""));
|
||||
if (app_node.contains("cmd"))
|
||||
ctx.cmd = parse_env_val(this_env, app_node.value("cmd", ""));
|
||||
if (app_node.contains("working-dir")) {
|
||||
ctx.working_dir = parse_env_val(this_env, app_node.value("working-dir", ""));
|
||||
#ifdef _WIN32
|
||||
// The working directory, unlike the command itself, should not be quoted.
|
||||
boost::erase_all(ctx.working_dir, "\"");
|
||||
ctx.working_dir += '\\';
|
||||
#endif
|
||||
}
|
||||
if (app_node.contains("image-path"))
|
||||
ctx.image_path = parse_env_val(this_env, app_node.value("image-path", ""));
|
||||
|
||||
ctx.elevated = app_node.value("elevated", false);
|
||||
ctx.auto_detach = app_node.value("auto-detach", true);
|
||||
ctx.wait_all = app_node.value("wait-all", true);
|
||||
ctx.exit_timeout = std::chrono::seconds { app_node.value("exit-timeout", 5) };
|
||||
ctx.virtual_display = app_node.value("virtual-display", false);
|
||||
ctx.scale_factor = app_node.value("scale-factor", 100);
|
||||
ctx.use_app_identity = app_node.value("use-app-identity", false);
|
||||
ctx.per_client_app_identity = app_node.value("per-client-app-identity", false);
|
||||
ctx.allow_client_commands = app_node.value("allow-client-commands", true);
|
||||
|
||||
// Calculate a unique application id.
|
||||
auto possible_ids = calculate_app_id(name, ctx.image_path, i++);
|
||||
if (ids.count(std::get<0>(possible_ids)) == 0) {
|
||||
ctx.id = std::get<0>(possible_ids);
|
||||
} else {
|
||||
ctx.id = std::get<1>(possible_ids);
|
||||
}
|
||||
ids.insert(ctx.id);
|
||||
|
||||
ctx.name = std::move(name);
|
||||
ctx.prep_cmds = std::move(prep_cmds);
|
||||
ctx.detached = std::move(detached);
|
||||
|
||||
apps.emplace_back(std::move(ctx));
|
||||
}
|
||||
} catch (std::exception &e) {
|
||||
BOOST_LOG(error) << e.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (config::input.enable_input_only_mode) {
|
||||
// Input Only entry
|
||||
{
|
||||
proc::ctx_t ctx;
|
||||
// ctx.uuid = ""; // We're not using uuid for this special entry
|
||||
ctx.name = "Virtual Display";
|
||||
ctx.image_path = parse_env_val(this_env, "virtual_desktop.png");
|
||||
ctx.virtual_display = true;
|
||||
ctx.name = "Remote Input";
|
||||
ctx.image_path = parse_env_val(this_env, "input_only.png");
|
||||
ctx.virtual_display = false;
|
||||
ctx.scale_factor = 100;
|
||||
ctx.use_app_identity = false;
|
||||
ctx.per_client_app_identity = false;
|
||||
@@ -1011,150 +1134,84 @@ namespace proc {
|
||||
}
|
||||
ids.insert(ctx.id);
|
||||
|
||||
input_only_app_id_str = ctx.id;
|
||||
input_only_app_id = util::from_view(ctx.id);
|
||||
|
||||
apps.emplace_back(std::move(ctx));
|
||||
}
|
||||
#endif
|
||||
|
||||
for (auto &[_, app_node] : apps_node) {
|
||||
// Terminate entry
|
||||
{
|
||||
proc::ctx_t ctx;
|
||||
// ctx.uuid = ""; // We're not using uuid for this special entry
|
||||
ctx.name = "Terminate";
|
||||
ctx.image_path = parse_env_val(this_env, "terminate.png");
|
||||
ctx.virtual_display = false;
|
||||
ctx.scale_factor = 100;
|
||||
ctx.use_app_identity = false;
|
||||
ctx.per_client_app_identity = false;
|
||||
ctx.allow_client_commands = false;
|
||||
|
||||
auto app_uuid = app_node.get_optional<std::string>("uuid"s);
|
||||
ctx.elevated = false;
|
||||
ctx.auto_detach = true;
|
||||
ctx.wait_all = true;
|
||||
ctx.exit_timeout = 5s;
|
||||
|
||||
if (!app_uuid) {
|
||||
// We need an upgrade to the app list
|
||||
try {
|
||||
BOOST_LOG(info) << "Migrating app list...";
|
||||
migrate_apps(&tree, nullptr);
|
||||
pt::write_json(file_name, tree);
|
||||
BOOST_LOG(info) << "Migration complete.";
|
||||
return parse(file_name);
|
||||
} catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "Error happened wilie migrating the app list: "sv << e.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
auto prep_nodes_opt = app_node.get_child_optional("prep-cmd"s);
|
||||
auto detached_nodes_opt = app_node.get_child_optional("detached"s);
|
||||
auto exclude_global_prep = app_node.get_optional<bool>("exclude-global-prep-cmd"s);
|
||||
auto output = app_node.get_optional<std::string>("output"s);
|
||||
auto name = parse_env_val(this_env, app_node.get<std::string>("name"s));
|
||||
auto cmd = app_node.get_optional<std::string>("cmd"s);
|
||||
auto image_path = app_node.get_optional<std::string>("image-path"s);
|
||||
auto working_dir = app_node.get_optional<std::string>("working-dir"s);
|
||||
auto elevated = app_node.get_optional<bool>("elevated"s);
|
||||
auto auto_detach = app_node.get_optional<bool>("auto-detach"s);
|
||||
auto wait_all = app_node.get_optional<bool>("wait-all"s);
|
||||
auto exit_timeout = app_node.get_optional<int>("exit-timeout"s);
|
||||
auto virtual_display = app_node.get_optional<bool>("virtual-display"s);
|
||||
auto resolution_scale_factor = app_node.get_optional<int>("scale-factor"s);
|
||||
auto use_app_identity = app_node.get_optional<bool>("use-app-identity"s);
|
||||
auto per_client_app_identity = app_node.get_optional<bool>("per-client-app-identity");
|
||||
auto allow_client_commands = app_node.get_optional<bool>("allow-client-commands");
|
||||
|
||||
ctx.uuid = app_uuid.value();
|
||||
|
||||
std::vector<proc::cmd_t> prep_cmds;
|
||||
if (!exclude_global_prep.value_or(false)) {
|
||||
prep_cmds.reserve(config::sunshine.prep_cmds.size());
|
||||
for (auto &prep_cmd : config::sunshine.prep_cmds) {
|
||||
auto do_cmd = parse_env_val(this_env, prep_cmd.do_cmd);
|
||||
auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_cmd);
|
||||
|
||||
prep_cmds.emplace_back(
|
||||
std::move(do_cmd),
|
||||
std::move(undo_cmd),
|
||||
std::move(prep_cmd.elevated)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (prep_nodes_opt) {
|
||||
auto &prep_nodes = *prep_nodes_opt;
|
||||
|
||||
prep_cmds.reserve(prep_cmds.size() + prep_nodes.size());
|
||||
for (auto &[_, prep_node] : prep_nodes) {
|
||||
auto do_cmd = prep_node.get_optional<std::string>("do"s);
|
||||
auto undo_cmd = prep_node.get_optional<std::string>("undo"s);
|
||||
auto elevated = prep_node.get_optional<bool>("elevated");
|
||||
|
||||
prep_cmds.emplace_back(
|
||||
parse_env_val(this_env, do_cmd.value_or("")),
|
||||
parse_env_val(this_env, undo_cmd.value_or("")),
|
||||
std::move(elevated.value_or(false))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> detached;
|
||||
if (detached_nodes_opt) {
|
||||
auto &detached_nodes = *detached_nodes_opt;
|
||||
|
||||
detached.reserve(detached_nodes.size());
|
||||
for (auto &[_, detached_val] : detached_nodes) {
|
||||
detached.emplace_back(parse_env_val(this_env, detached_val.get_value<std::string>()));
|
||||
}
|
||||
}
|
||||
|
||||
if (output) {
|
||||
ctx.output = parse_env_val(this_env, *output);
|
||||
}
|
||||
|
||||
if (cmd) {
|
||||
ctx.cmd = parse_env_val(this_env, *cmd);
|
||||
}
|
||||
|
||||
if (working_dir) {
|
||||
ctx.working_dir = parse_env_val(this_env, *working_dir);
|
||||
#ifdef _WIN32
|
||||
// The working directory, unlike the command itself, should not be quoted
|
||||
// when it contains spaces. Unlike POSIX, Windows forbids quotes in paths,
|
||||
// so we can safely strip them all out here to avoid confusing the user.
|
||||
boost::erase_all(ctx.working_dir, "\"");
|
||||
ctx.working_dir += '\\';
|
||||
#endif
|
||||
}
|
||||
|
||||
if (image_path) {
|
||||
ctx.image_path = parse_env_val(this_env, *image_path);
|
||||
}
|
||||
|
||||
ctx.elevated = elevated.value_or(false);
|
||||
ctx.auto_detach = auto_detach.value_or(true);
|
||||
ctx.wait_all = wait_all.value_or(true);
|
||||
ctx.exit_timeout = std::chrono::seconds {exit_timeout.value_or(5)};
|
||||
ctx.virtual_display = virtual_display.value_or(false);
|
||||
ctx.scale_factor = resolution_scale_factor.value_or(100);
|
||||
ctx.use_app_identity = use_app_identity.value_or(false);
|
||||
ctx.per_client_app_identity = per_client_app_identity.value_or(false);
|
||||
ctx.allow_client_commands = allow_client_commands.value_or(true);
|
||||
|
||||
auto possible_ids = calculate_app_id(name, ctx.image_path, i++);
|
||||
auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++);
|
||||
if (ids.count(std::get<0>(possible_ids)) == 0) {
|
||||
// Avoid using index to generate id if possible
|
||||
ctx.id = std::get<0>(possible_ids);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// Fallback to include index on collision
|
||||
ctx.id = std::get<1>(possible_ids);
|
||||
}
|
||||
ids.insert(ctx.id);
|
||||
|
||||
ctx.name = std::move(name);
|
||||
ctx.prep_cmds = std::move(prep_cmds);
|
||||
ctx.detached = std::move(detached);
|
||||
terminate_app_id_str = ctx.id;
|
||||
terminate_app_id = util::from_view(ctx.id);
|
||||
|
||||
apps.emplace_back(std::move(ctx));
|
||||
}
|
||||
|
||||
return proc::proc_t {
|
||||
std::move(this_env),
|
||||
std::move(apps)
|
||||
};
|
||||
} catch (std::exception &e) {
|
||||
BOOST_LOG(error) << e.what();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
// Virtual Display entry
|
||||
#ifdef _WIN32
|
||||
if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) {
|
||||
proc::ctx_t ctx;
|
||||
// ctx.uuid = ""; // We're not using uuid for this special entry
|
||||
ctx.name = "Virtual Display";
|
||||
ctx.image_path = parse_env_val(this_env, "virtual_desktop.png");
|
||||
ctx.virtual_display = true;
|
||||
ctx.scale_factor = 100;
|
||||
ctx.use_app_identity = false;
|
||||
ctx.per_client_app_identity = false;
|
||||
ctx.allow_client_commands = false;
|
||||
|
||||
ctx.elevated = false;
|
||||
ctx.auto_detach = true;
|
||||
ctx.wait_all = true;
|
||||
ctx.exit_timeout = 5s;
|
||||
|
||||
auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++);
|
||||
if (ids.count(std::get<0>(possible_ids)) == 0) {
|
||||
// Avoid using index to generate id if possible
|
||||
ctx.id = std::get<0>(possible_ids);
|
||||
}
|
||||
else {
|
||||
// Fallback to include index on collision
|
||||
ctx.id = std::get<1>(possible_ids);
|
||||
}
|
||||
ids.insert(ctx.id);
|
||||
|
||||
apps.emplace_back(std::move(ctx));
|
||||
}
|
||||
#endif
|
||||
|
||||
return proc::proc_t {
|
||||
std::move(this_env),
|
||||
std::move(apps)
|
||||
};
|
||||
}
|
||||
|
||||
void refresh(const std::string &file_name) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// lib includes
|
||||
#include <boost/process/v1.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
// local includes
|
||||
#include "config.h"
|
||||
@@ -155,7 +156,7 @@ namespace proc {
|
||||
|
||||
std::string validate_app_image_path(std::string app_image_path);
|
||||
void refresh(const std::string &file_name);
|
||||
void migrate_apps(boost::property_tree::ptree* fileTree_p, boost::property_tree::ptree* inputTree_p);
|
||||
void migrate_apps(nlohmann::json* fileTree_p, nlohmann::json* inputTree_p);
|
||||
std::optional<proc::proc_t> parse(const std::string &file_name);
|
||||
|
||||
/**
|
||||
|
||||
@@ -399,7 +399,7 @@
|
||||
<button @click="showEditForm = false" class="btn btn-secondary m-2">
|
||||
{{ $t('_common.cancel') }}
|
||||
</button>
|
||||
<button class="btn btn-primary m-2" @click="save">{{ $t('_common.save') }}</button>
|
||||
<button class="btn btn-primary m-2" :disabled="!editForm.name.trim()" @click="save">{{ $t('_common.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -418,18 +418,18 @@
|
||||
import { Dropdown } from 'bootstrap/dist/js/bootstrap'
|
||||
|
||||
const newApp = {
|
||||
name: "",
|
||||
output: "",
|
||||
cmd: [],
|
||||
"name": "New App",
|
||||
"output": "",
|
||||
"cmd": "",
|
||||
"exclude-global-prep-cmd": false,
|
||||
elevated: false,
|
||||
"elevated": false,
|
||||
"auto-detach": true,
|
||||
"wait-all": true,
|
||||
"exit-timeout": 5,
|
||||
"prep-cmd": [],
|
||||
detached: [],
|
||||
"detached": [],
|
||||
"image-path": "",
|
||||
"scale-factor": "100",
|
||||
"scale-factor": 100,
|
||||
"use-app-identity": false,
|
||||
"per-client-app-identity": false,
|
||||
"allow-client-commands": true,
|
||||
@@ -526,11 +526,19 @@
|
||||
"Are you sure to delete " + app.name + "?"
|
||||
);
|
||||
if (resp) {
|
||||
this.actionDisabled = true;
|
||||
fetch("./api/apps/delete?uuid=" + app.uuid, {
|
||||
credentials: 'include',
|
||||
method: "POST"
|
||||
}).then((r) => {
|
||||
if (r.status === 200) document.location.reload();
|
||||
}).then((r) => r.json())
|
||||
.then((r) => {
|
||||
if (!r.status) {
|
||||
alert("Delete failed! " + r.error);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.actionDisabled = false;
|
||||
this.loadApps();
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -643,14 +651,26 @@
|
||||
},
|
||||
save() {
|
||||
this.editForm.name = this.editForm.name.trim();
|
||||
if (!this.editForm.name) {
|
||||
return;
|
||||
}
|
||||
this.editForm["exit-timeout"] = parseInt(this.editForm["exit-timeout"]) || 5
|
||||
this.editForm["scale-factor"] = parseInt(this.editForm["scale-factor"]) || 100
|
||||
this.editForm["image-path"] = this.editForm["image-path"].toString().trim().replace(/"/g, '');
|
||||
delete this.editForm["id"];
|
||||
fetch("./api/apps", {
|
||||
credentials: 'include',
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.editForm),
|
||||
}).then((r) => {
|
||||
if (r.status === 200) document.location.reload();
|
||||
}).then((r) => r.json())
|
||||
.then((r) => {
|
||||
if (!r.status) {
|
||||
alert(this.$t('apps.save_failed') + r.error);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.showEditForm = false;
|
||||
this.loadApps();
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"cmd_prep_name": "Command Preparations",
|
||||
"covers_found": "Covers Found",
|
||||
"delete": "Delete",
|
||||
"delete_failed": "App delete failed: ",
|
||||
"detached_cmds": "Detached Commands",
|
||||
"detached_cmds_add": "Add Detached Command",
|
||||
"detached_cmds_desc": "A list of commands to be run in the background.",
|
||||
@@ -91,6 +92,7 @@
|
||||
"per_client_app_identity": "Per Client App Identity",
|
||||
"per_client_app_identity_desc": "Separate the app's identity per-client. Useful when you want different virtual display configurations on this specific app for different clients",
|
||||
"run_as_desc": "This can be necessary for some applications that require administrator permissions to run properly. Might cause URL schemes to fail.",
|
||||
"save_failed": "Failed to save app: ",
|
||||
"wait_all": "Continue streaming until all app processes exit",
|
||||
"wait_all_desc": "This will continue streaming until all processes started by the app have terminated. When unchecked, streaming will stop when the initial app process exits, even if other app processes are still running.",
|
||||
"working_dir": "Working Directory",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"cmd_prep_name": "命令准备工作",
|
||||
"covers_found": "找到的封面",
|
||||
"delete": "删除",
|
||||
"delete_failed": "APP删除失败:",
|
||||
"detached_cmds": "独立命令",
|
||||
"detached_cmds_add": "添加独立命令",
|
||||
"detached_cmds_desc": "要在后台运行的命令列表。",
|
||||
@@ -90,6 +91,7 @@
|
||||
"per_client_app_identity": "按客户端区分 App 身份",
|
||||
"per_client_app_identity_desc": "当你希望在使用此 App 时每个客户端都有不同的虚拟显示器组合配置时有用。",
|
||||
"run_as_desc": "这可能是某些需要管理员权限才能正常运行的应用程序所必需的。可能会导致 URL schemes 无法正常启动。",
|
||||
"save_failed": "保存APP失败:",
|
||||
"wait_all": "继续串流直到所有应用进程退出",
|
||||
"wait_all_desc": "这将继续串流直到应用程序启动的所有进程终止。 当未选中时,串流将在初始应用进程终止时停止,即使其他应用进程仍在运行。",
|
||||
"working_dir": "工作目录",
|
||||
|
||||
Reference in New Issue
Block a user