diff --git a/docs/modules/dev/partials/spec.json b/docs/modules/dev/partials/spec.json index 2ce6160a..1c771505 100644 --- a/docs/modules/dev/partials/spec.json +++ b/docs/modules/dev/partials/spec.json @@ -215,6 +215,45 @@ } } }, + "/api/v1/runners/start": { + "post": { + "summary": "Start a runner in a given session", + "description": "", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__RunnerStartRequest" + } + } + }, + "description": "", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericSuccessResponse" + } + } + }, + "description": "" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/wolf__api__GenericErrorResponse" + } + } + }, + "description": "" + } + } + } + }, "/api/v1/sessions/add": { "post": { "summary": "Create a new stream session", @@ -347,6 +386,16 @@ "hevc_gst_pipeline": { "type": "string" }, + "icon_png_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, "id": { "type": "string" }, @@ -559,6 +608,35 @@ "success" ] }, + "wolf__api__RunnerStartRequest": { + "type": "object", + "properties": { + "runner": { + "anyOf": [ + { + "$ref": "#/components/schemas/wolf__config__AppCMD__tagged" + }, + { + "$ref": "#/components/schemas/wolf__config__AppDocker__tagged" + }, + { + "$ref": "#/components/schemas/wolf__config__AppChildSession__tagged" + } + ] + }, + "session_id": { + "type": "string" + }, + "stop_stream_when_over": { + "type": "boolean" + } + }, + "required": [ + "runner", + "session_id", + "stop_stream_when_over" + ] + }, "wolf__api__StreamSessionListResponse": { "type": "object", "properties": { diff --git a/src/moonlight-server/api/api.hpp b/src/moonlight-server/api/api.hpp index adf9bd0a..9db21404 100644 --- a/src/moonlight-server/api/api.hpp +++ b/src/moonlight-server/api/api.hpp @@ -62,6 +62,12 @@ struct StreamSessionStopRequest { std::string session_id; }; +struct RunnerStartRequest { + bool stop_stream_when_over; + rfl::TaggedUnion<"type", wolf::config::AppCMD, wolf::config::AppDocker, wolf::config::AppChildSession> runner; + std::string session_id; +}; + struct UnixSocket { boost::asio::local::stream_protocol::socket socket; bool is_alive = true; @@ -93,6 +99,8 @@ class UnixSocketServer { void endpoint_StreamSessionPause(const HTTPRequest &req, std::shared_ptr socket); void endpoint_StreamSessionStop(const HTTPRequest &req, std::shared_ptr socket); + void endpoint_RunnerStart(const HTTPRequest &req, std::shared_ptr socket); + void sse_broadcast(const std::string &payload); void sse_keepalive(const boost::system::error_code &e); diff --git a/src/moonlight-server/api/endpoints.cpp b/src/moonlight-server/api/endpoints.cpp index ddf49046..3a99c852 100644 --- a/src/moonlight-server/api/endpoints.cpp +++ b/src/moonlight-server/api/endpoints.cpp @@ -202,4 +202,28 @@ void UnixSocketServer::endpoint_StreamSessionStop(const HTTPRequest &req, std::s } } +void UnixSocketServer::endpoint_RunnerStart(const wolf::api::HTTPRequest &req, std::shared_ptr socket) { + auto event = rfl::json::read(req.body); + if (event) { + auto session = state::get_session_by_id(this->state_->app_state->running_sessions->load(), + std::stoul(event.value().session_id)); + if (!session) { + logs::log(logs::warning, "[API] Invalid session_id: {}", event.value().session_id); + auto res = GenericErrorResponse{.error = "Invalid session_id"}; + send_http(socket, 500, rfl::json::write(res)); + return; + } + + auto runner = state::get_runner(event.value().runner, this->state_->app_state->event_bus); + state_->app_state->event_bus->fire_event(immer::box( + events::StartRunner{.stop_stream_when_over = event.value().stop_stream_when_over, + .runner = runner, + .stream_session = immer::box(*session)})); + } else { + logs::log(logs::warning, "[API] Invalid event: {} - {}", req.body, event.error()->what()); + auto res = GenericErrorResponse{.error = event.error()->what()}; + send_http(socket, 500, rfl::json::write(res)); + } +} + } // namespace wolf::api \ No newline at end of file diff --git a/src/moonlight-server/api/openapi.cpp b/src/moonlight-server/api/openapi.cpp index 6616f90e..60750c90 100644 --- a/src/moonlight-server/api/openapi.cpp +++ b/src/moonlight-server/api/openapi.cpp @@ -9,7 +9,7 @@ struct OpenAPIComponents { struct OpenAPISchema { std::string openapi = "3.1.0"; rfl::Object info = {}; - rfl::Object server = {}; + std::vector> servers = {}; rfl::Object> paths = {}; OpenAPIComponents components = {}; }; @@ -27,8 +27,10 @@ template std::string HTTPServer::openapi_schema() const { schema.info["description"] = "API for the Wolf server"; schema.info["version"] = "0.1"; - schema.server["url"] = "http://localhost/"; - schema.server["description"] = "Unix socket server"; + rfl::Object server = {}; + server["url"] = "http://localhost/"; + server["description"] = "Local development server"; + schema.servers.push_back(server); /** * Takes a json schema in string form and returns a valid OpenAPI json object for that schema. diff --git a/src/moonlight-server/api/unix_socket_server.cpp b/src/moonlight-server/api/unix_socket_server.cpp index 895f3fd8..4bae5ce1 100644 --- a/src/moonlight-server/api/unix_socket_server.cpp +++ b/src/moonlight-server/api/unix_socket_server.cpp @@ -147,6 +147,16 @@ UnixSocketServer::UnixSocketServer(boost::asio::io_context &io_context, .handler = [this](auto req, auto socket) { endpoint_StreamSessionStop(req, socket); }, }); + state_->http.add(HTTPMethod::POST, + "/api/v1/runners/start", + { + .summary = "Start a runner in a given session", + .request_description = APIDescription{.json_schema = rfl::json::to_schema()}, + .response_description = {{200, {.json_schema = rfl::json::to_schema()}}, + {500, {.json_schema = rfl::json::to_schema()}}}, + .handler = [this](auto req, auto socket) { endpoint_RunnerStart(req, socket); }, + }); + /** * OpenAPI schema */ diff --git a/src/moonlight-server/events/events.hpp b/src/moonlight-server/events/events.hpp index 25ab66b9..d9fef9d9 100644 --- a/src/moonlight-server/events/events.hpp +++ b/src/moonlight-server/events/events.hpp @@ -48,7 +48,7 @@ struct Runner { virtual void run(std::size_t session_id, std::string_view app_state_folder, - std::shared_ptr plugged_devices_queue, + const std::shared_ptr &plugged_devices_queue, const immer::array &virtual_inputs, const immer::array> &paths, const immer::map &env_variables, @@ -161,6 +161,12 @@ struct RTPAudioPingEvent { struct StreamSession; +struct StartRunner { + bool stop_stream_when_over = false; + std::shared_ptr runner; + immer::box stream_session; +}; + using EventBusHandlers = dp::handler_registration, immer::box, immer::box, @@ -172,7 +178,8 @@ using EventBusHandlers = dp::handler_registration, immer::box, immer::box, immer::box, - immer::box>; + immer::box, + immer::box>; using EventBusType = dp::event_bus, immer::box, immer::box, @@ -184,7 +191,8 @@ using EventBusType = dp::event_bus, immer::box, immer::box, immer::box, - immer::box>; + immer::box, + immer::box>; using EventsVariant = std::variant, immer::box, immer::box, @@ -196,7 +204,8 @@ using EventsVariant = std::variant, immer::box, immer::box, immer::box, - immer::box>; + immer::box, + immer::box>; /** * A StreamSession is created when a Moonlight user call `launch` @@ -230,6 +239,9 @@ struct StreamSession { std::shared_ptr> wayland_display = std::make_shared>(); + std::shared_ptr>> audio_sink = + std::make_shared>>(); + // virtual devices std::shared_ptr> mouse = std::make_shared>(); std::shared_ptr> keyboard = std::make_shared>(); diff --git a/src/moonlight-server/events/reflectors.hpp b/src/moonlight-server/events/reflectors.hpp index 6299ed39..2bd621b7 100644 --- a/src/moonlight-server/events/reflectors.hpp +++ b/src/moonlight-server/events/reflectors.hpp @@ -77,6 +77,20 @@ template <> struct Reflector { } }; +template <> struct Reflector { + struct ReflType { + bool stop_stream_when_over; + rfl::TaggedUnion<"type", AppCMD, AppDocker, AppChildSession> runner; + std::string session_id; + }; + + static ReflType from(const events::StartRunner &v) { + return {.stop_stream_when_over = v.stop_stream_when_over, + .runner = v.runner->serialize(), + .session_id = std::to_string(v.stream_session->session_id)}; + } +}; + template <> struct Reflector { struct ReflType { std::string app_id; diff --git a/src/moonlight-server/runners/child_session.hpp b/src/moonlight-server/runners/child_session.hpp index e6dcac4a..38527c24 100644 --- a/src/moonlight-server/runners/child_session.hpp +++ b/src/moonlight-server/runners/child_session.hpp @@ -19,7 +19,7 @@ class RunChildSession : public events::Runner { void run(std::size_t session_id, std::string_view app_state_folder, - std::shared_ptr plugged_devices_queue, + const std::shared_ptr &plugged_devices_queue, const immer::array &virtual_inputs, const immer::array> &paths, const immer::map &env_variables, diff --git a/src/moonlight-server/runners/docker.cpp b/src/moonlight-server/runners/docker.cpp index c8b8b84f..674bb48d 100644 --- a/src/moonlight-server/runners/docker.cpp +++ b/src/moonlight-server/runners/docker.cpp @@ -15,7 +15,7 @@ void create_udev_hw_files(std::filesystem::path base_hw_db_path, void RunDocker::run(std::size_t session_id, std::string_view app_state_folder, - std::shared_ptr plugged_devices_queue, + const std::shared_ptr &plugged_devices_queue, const immer::array &virtual_inputs, const immer::array> &paths, const immer::map &env_variables, diff --git a/src/moonlight-server/runners/docker.hpp b/src/moonlight-server/runners/docker.hpp index a45f8c83..b4836e69 100644 --- a/src/moonlight-server/runners/docker.hpp +++ b/src/moonlight-server/runners/docker.hpp @@ -86,7 +86,7 @@ class RunDocker : public events::Runner { void run(std::size_t session_id, std::string_view app_state_folder, - std::shared_ptr plugged_devices_queue, + const std::shared_ptr &plugged_devices_queue, const immer::array &virtual_inputs, const immer::array> &paths, const immer::map &env_variables, diff --git a/src/moonlight-server/runners/process.cpp b/src/moonlight-server/runners/process.cpp index 64070d4b..5ef39f7d 100644 --- a/src/moonlight-server/runners/process.cpp +++ b/src/moonlight-server/runners/process.cpp @@ -12,7 +12,7 @@ using namespace wolf::core::events; void RunProcess::run(std::size_t session_id, std::string_view app_state_folder, - std::shared_ptr plugged_devices_queue, + const std::shared_ptr &plugged_devices_queue, const immer::array &virtual_inputs, const immer::array> &paths, const immer::map &env_variables, diff --git a/src/moonlight-server/runners/process.hpp b/src/moonlight-server/runners/process.hpp index b816c5b5..2af9cc43 100644 --- a/src/moonlight-server/runners/process.hpp +++ b/src/moonlight-server/runners/process.hpp @@ -19,7 +19,7 @@ class RunProcess : public events::Runner { void run(std::size_t session_id, std::string_view app_state_folder, - std::shared_ptr plugged_devices_queue, + const std::shared_ptr &plugged_devices_queue, const immer::array &virtual_inputs, const immer::array> &paths, const immer::map &env_variables, diff --git a/src/moonlight-server/wolf.cpp b/src/moonlight-server/wolf.cpp index 469c9a0b..1c75c306 100644 --- a/src/moonlight-server/wolf.cpp +++ b/src/moonlight-server/wolf.cpp @@ -172,11 +172,15 @@ auto setup_sessions_handlers(const immer::box &app_state, // Run process and our custom wayland as soon as a new StreamSession is created handlers.push_back(app_state->event_bus->register_handler>( [=](const immer::box &session) { - auto render_node = session->app->render_node; + /* Initialise plugged device queue */ + auto devices_q = std::make_shared(); + plugged_devices_queue->update( + [=](const session_devices map) { return map.set(session->session_id, devices_q); }); if (session->app->start_virtual_compositor) { logs::log(logs::debug, "[STREAM_SESSION] Create wayland compositor"); + auto render_node = session->app->render_node; auto wl_state = virtual_display::create_wayland_display({}, render_node); virtual_display::set_resolution( *wl_state, @@ -198,25 +202,57 @@ auto setup_sessions_handlers(const immer::box &app_state, .refreshRate = session->display_mode.refreshRate}, session->event_bus); }).detach(); + } else { + // Create virtual devices + auto mouse = input::Mouse::create(); + if (!mouse) { + logs::log(logs::error, "Failed to create mouse: {}", mouse.getErrorMessage()); + } else { + auto mouse_ptr = input::Mouse(std::move(*mouse)); + devices_q->push(immer::box( + events::PlugDeviceEvent{.session_id = session->session_id, + .udev_events = mouse_ptr.get_udev_events(), + .udev_hw_db_entries = mouse_ptr.get_udev_hw_db_entries()})); + session->mouse->emplace(std::move(mouse_ptr)); + } + + auto keyboard = input::Keyboard::create(); + if (!keyboard) { + logs::log(logs::error, "Failed to create keyboard: {}", keyboard.getErrorMessage()); + } else { + auto keyboard_ptr = input::Keyboard(std::move(*keyboard)); + devices_q->push(immer::box( + events::PlugDeviceEvent{.session_id = session->session_id, + .udev_events = keyboard_ptr.get_udev_events(), + .udev_hw_db_entries = keyboard_ptr.get_udev_hw_db_entries()})); + session->keyboard->emplace(std::move(keyboard_ptr)); + } } + /* Create audio virtual sink */ + logs::log(logs::debug, "[STREAM_SESSION] Create virtual audio sink"); + auto pulse_sink_name = fmt::format("virtual_sink_{}", session->session_id); + std::shared_ptr v_device; + if (audio_server && audio_server->server) { + v_device = audio::create_virtual_sink( + audio_server->server, + audio::AudioDevice{.sink_name = pulse_sink_name, + .mode = state::get_audio_mode(session->audio_channel_count, true)}); + session->audio_sink->store(v_device); + } + + session->event_bus->fire_event(immer::box( + events::StartRunner{.stop_stream_when_over = true, .runner = session->app->runner, .stream_session = session + + })); + })); + + /* Start runner */ + handlers.push_back(app_state->event_bus->register_handler>( + [=](const immer::box &run_session) { // Start selected app on a separate thread std::thread([=]() { - /* Create audio virtual sink */ - logs::log(logs::debug, "[STREAM_SESSION] Create virtual audio sink"); - auto pulse_sink_name = fmt::format("virtual_sink_{}", session->session_id); - std::shared_ptr v_device; - if (audio_server && audio_server->server) { - v_device = audio::create_virtual_sink( - audio_server->server, - audio::AudioDevice{.sink_name = pulse_sink_name, - .mode = state::get_audio_mode(session->audio_channel_count, true)}); - } - - /* Initialise plugged device queue */ - auto devices_q = std::make_shared(); - plugged_devices_queue->update( - [=](const session_devices map) { return map.set(session->session_id, devices_q); }); + auto session = run_session->stream_session; /* Setup devices paths */ auto all_devices = immer::array_transient(); @@ -228,6 +264,7 @@ auto setup_sessions_handlers(const immer::box &app_state, immer::map_transient full_env; full_env.set("XDG_RUNTIME_DIR", runtime_dir); + auto pulse_sink_name = fmt::format("virtual_sink_{}", session->session_id); auto audio_server_name = audio_server ? audio::get_server_name(audio_server->server) : ""; full_env.set("PULSE_SINK", pulse_sink_name); full_env.set("PULSE_SOURCE", pulse_sink_name + ".monitor"); @@ -256,37 +293,13 @@ auto setup_sessions_handlers(const immer::box &app_state, full_env.set(utils::to_string(split[0]), utils::to_string(split[1])); } - } else { - // Create virtual devices - auto mouse = input::Mouse::create(); - if (!mouse) { - logs::log(logs::error, "Failed to create mouse: {}", mouse.getErrorMessage()); - } else { - auto mouse_ptr = input::Mouse(std::move(*mouse)); - devices_q->push(immer::box( - events::PlugDeviceEvent{.session_id = session->session_id, - .udev_events = mouse_ptr.get_udev_events(), - .udev_hw_db_entries = mouse_ptr.get_udev_hw_db_entries()})); - session->mouse->emplace(std::move(mouse_ptr)); - } - - auto keyboard = input::Keyboard::create(); - if (!keyboard) { - logs::log(logs::error, "Failed to create keyboard: {}", keyboard.getErrorMessage()); - } else { - auto keyboard_ptr = input::Keyboard(std::move(*keyboard)); - devices_q->push(immer::box( - events::PlugDeviceEvent{.session_id = session->session_id, - .udev_events = keyboard_ptr.get_udev_events(), - .udev_hw_db_entries = keyboard_ptr.get_udev_hw_db_entries()})); - session->keyboard->emplace(std::move(keyboard_ptr)); - } } /* Adding custom state folder */ mounted_paths.push_back({session->app_state_folder, "/home/retro"}); /* GPU specific adjustments */ + auto render_node = session->app->render_node; auto additional_devices = linked_devices(render_node); std::copy(additional_devices.begin(), additional_devices.end(), std::back_inserter(all_devices)); @@ -300,26 +313,29 @@ auto setup_sessions_handlers(const immer::box &app_state, full_env.set("INTEL_DEBUG", "norbc"); // see: https://github.com/games-on-whales/wolf/issues/50 } + auto devices_q = plugged_devices_queue->load()->find(session->session_id); + /* Finally run the app, this will stop here until over */ - session->app->runner->run(session->session_id, - session->app_state_folder, - devices_q, - all_devices.persistent(), - mounted_paths.persistent(), - full_env.persistent(), - render_node); - - /* App exited, cleanup */ - logs::log(logs::debug, "[STREAM_SESSION] Remove virtual audio sink"); - if (audio_server && audio_server->server) { - audio::delete_virtual_sink(audio_server->server, v_device); - } + run_session->runner->run(session->session_id, + session->app_state_folder, + *devices_q, + all_devices.persistent(), + mounted_paths.persistent(), + full_env.persistent(), + render_node); + + if (run_session->stop_stream_when_over) { + /* App exited, cleanup */ + logs::log(logs::debug, "[STREAM_SESSION] Remove virtual audio sink"); + if (session->audio_sink) { + audio::delete_virtual_sink(audio_server->server, session->audio_sink->load()); + } - session->wayland_display->store(nullptr); + session->wayland_display->store(nullptr); - /* When the app closes there's no point in keeping the stream running */ - app_state->event_bus->fire_event( - immer::box(events::StopStreamEvent{.session_id = session->session_id})); + app_state->event_bus->fire_event( + immer::box(events::StopStreamEvent{.session_id = session->session_id})); + } }).detach(); }));