summaryrefslogtreecommitdiffstats
path: root/input/ipc-win.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--input/ipc-win.c509
1 files changed, 509 insertions, 0 deletions
diff --git a/input/ipc-win.c b/input/ipc-win.c
new file mode 100644
index 0000000..b0200ea
--- /dev/null
+++ b/input/ipc-win.c
@@ -0,0 +1,509 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <windows.h>
+#include <sddl.h>
+
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "osdep/windows_utils.h"
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "input/input.h"
+#include "libmpv/client.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "player/client.h"
+
+struct mp_ipc_ctx {
+ struct mp_log *log;
+ struct mp_client_api *client_api;
+ const wchar_t *path;
+
+ mp_thread thread;
+ HANDLE death_event;
+};
+
+struct client_arg {
+ struct mp_log *log;
+ struct mpv_handle *client;
+
+ char *client_name;
+ HANDLE client_h;
+ bool writable;
+ OVERLAPPED write_ol;
+};
+
+// Get a string SID representing the current user. Must be freed by LocalFree.
+static char *get_user_sid(void)
+{
+ char *ssid = NULL;
+ TOKEN_USER *info = NULL;
+ HANDLE t;
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t))
+ goto done;
+
+ DWORD info_len;
+ if (!GetTokenInformation(t, TokenUser, NULL, 0, &info_len) &&
+ GetLastError() != ERROR_INSUFFICIENT_BUFFER)
+ goto done;
+
+ info = talloc_size(NULL, info_len);
+ if (!GetTokenInformation(t, TokenUser, info, info_len, &info_len))
+ goto done;
+ if (!info->User.Sid)
+ goto done;
+
+ ConvertSidToStringSidA(info->User.Sid, &ssid);
+done:
+ if (t)
+ CloseHandle(t);
+ talloc_free(info);
+ return ssid;
+}
+
+// Get a string SID for the process integrity level. Must be freed by LocalFree.
+static char *get_integrity_sid(void)
+{
+ char *ssid = NULL;
+ TOKEN_MANDATORY_LABEL *info = NULL;
+ HANDLE t;
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t))
+ goto done;
+
+ DWORD info_len;
+ if (!GetTokenInformation(t, TokenIntegrityLevel, NULL, 0, &info_len) &&
+ GetLastError() != ERROR_INSUFFICIENT_BUFFER)
+ goto done;
+
+ info = talloc_size(NULL, info_len);
+ if (!GetTokenInformation(t, TokenIntegrityLevel, info, info_len, &info_len))
+ goto done;
+ if (!info->Label.Sid)
+ goto done;
+
+ ConvertSidToStringSidA(info->Label.Sid, &ssid);
+done:
+ if (t)
+ CloseHandle(t);
+ talloc_free(info);
+ return ssid;
+}
+
+// Create a security descriptor that only grants access to processes running
+// under the current user at the current integrity level or higher
+static PSECURITY_DESCRIPTOR create_restricted_sd(void)
+{
+ char *user_sid = get_user_sid();
+ char *integrity_sid = get_integrity_sid();
+ if (!user_sid || !integrity_sid)
+ return NULL;
+
+ char *sddl = talloc_asprintf(NULL,
+ "O:%s" // Set the owner to user_sid
+ "D:(A;;GRGW;;;%s)" // Grant GENERIC_{READ,WRITE} access to user_sid
+ "S:(ML;;NRNWNX;;;%s)", // Disallow read, write and execute permissions
+ // to integrity levels below integrity_sid
+ user_sid, user_sid, integrity_sid);
+ LocalFree(user_sid);
+ LocalFree(integrity_sid);
+
+ PSECURITY_DESCRIPTOR sd = NULL;
+ ConvertStringSecurityDescriptorToSecurityDescriptorA(sddl, SDDL_REVISION_1,
+ &sd, NULL);
+ talloc_free(sddl);
+
+ return sd;
+}
+
+static void wakeup_cb(void *d)
+{
+ HANDLE event = d;
+ SetEvent(event);
+}
+
+// Wrapper for ReadFile that treats ERROR_IO_PENDING as success
+static DWORD async_read(HANDLE file, void *buf, unsigned size, OVERLAPPED* ol)
+{
+ DWORD err = ReadFile(file, buf, size, NULL, ol) ? 0 : GetLastError();
+ return err == ERROR_IO_PENDING ? 0 : err;
+}
+
+// Wrapper for WriteFile that treats ERROR_IO_PENDING as success
+static DWORD async_write(HANDLE file, const void *buf, unsigned size, OVERLAPPED* ol)
+{
+ DWORD err = WriteFile(file, buf, size, NULL, ol) ? 0 : GetLastError();
+ return err == ERROR_IO_PENDING ? 0 : err;
+}
+
+static bool pipe_error_is_fatal(DWORD error)
+{
+ switch (error) {
+ case 0:
+ case ERROR_HANDLE_EOF:
+ case ERROR_BROKEN_PIPE:
+ case ERROR_PIPE_NOT_CONNECTED:
+ case ERROR_NO_DATA:
+ return false;
+ }
+ return true;
+}
+
+static DWORD ipc_write_str(struct client_arg *arg, const char *buf)
+{
+ DWORD error = 0;
+
+ if ((error = async_write(arg->client_h, buf, strlen(buf), &arg->write_ol)))
+ goto done;
+ if (!GetOverlappedResult(arg->client_h, &arg->write_ol, &(DWORD){0}, TRUE)) {
+ error = GetLastError();
+ goto done;
+ }
+
+done:
+ if (pipe_error_is_fatal(error)) {
+ MP_VERBOSE(arg, "Error writing to pipe: %s\n",
+ mp_HRESULT_to_str(HRESULT_FROM_WIN32(error)));
+ }
+
+ if (error)
+ arg->writable = false;
+ return error;
+}
+
+static void report_read_error(struct client_arg *arg, DWORD error)
+{
+ // Only report the error if it's not just due to the pipe closing
+ if (pipe_error_is_fatal(error)) {
+ MP_ERR(arg, "Error reading from pipe: %s\n",
+ mp_HRESULT_to_str(HRESULT_FROM_WIN32(error)));
+ } else {
+ MP_VERBOSE(arg, "Client disconnected\n");
+ }
+}
+
+static MP_THREAD_VOID client_thread(void *p)
+{
+ struct client_arg *arg = p;
+ char buf[4096];
+ HANDLE wakeup_event = CreateEventW(NULL, TRUE, FALSE, NULL);
+ OVERLAPPED ol = { .hEvent = CreateEventW(NULL, TRUE, TRUE, NULL) };
+ bstr client_msg = { talloc_strdup(NULL, ""), 0 };
+ DWORD ioerr = 0;
+ DWORD r;
+
+ char *tname = talloc_asprintf(NULL, "ipc/%s", arg->client_name);
+ mp_thread_set_name(tname);
+ talloc_free(tname);
+
+ arg->write_ol.hEvent = CreateEventW(NULL, TRUE, TRUE, NULL);
+ if (!wakeup_event || !ol.hEvent || !arg->write_ol.hEvent) {
+ MP_ERR(arg, "Couldn't create events\n");
+ goto done;
+ }
+
+ MP_VERBOSE(arg, "Client connected\n");
+
+ mpv_set_wakeup_callback(arg->client, wakeup_cb, wakeup_event);
+
+ // Do the first read operation on the pipe
+ if ((ioerr = async_read(arg->client_h, buf, 4096, &ol))) {
+ report_read_error(arg, ioerr);
+ goto done;
+ }
+
+ while (1) {
+ HANDLE handles[] = { wakeup_event, ol.hEvent };
+ int n = WaitForMultipleObjects(2, handles, FALSE, 0);
+ if (n == WAIT_TIMEOUT)
+ n = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
+
+ switch (n) {
+ case WAIT_OBJECT_0: // wakeup_event
+ ResetEvent(wakeup_event);
+
+ while (1) {
+ mpv_event *event = mpv_wait_event(arg->client, 0);
+
+ if (event->event_id == MPV_EVENT_NONE)
+ break;
+
+ if (event->event_id == MPV_EVENT_SHUTDOWN)
+ goto done;
+
+ if (!arg->writable)
+ continue;
+
+ char *event_msg = mp_json_encode_event(event);
+ if (!event_msg) {
+ MP_ERR(arg, "Encoding error\n");
+ goto done;
+ }
+
+ ipc_write_str(arg, event_msg);
+ talloc_free(event_msg);
+ }
+
+ break;
+ case WAIT_OBJECT_0 + 1: // ol.hEvent
+ // Complete the read operation on the pipe
+ if (!GetOverlappedResult(arg->client_h, &ol, &r, TRUE)) {
+ report_read_error(arg, GetLastError());
+ goto done;
+ }
+
+ bstr_xappend(NULL, &client_msg, (bstr){buf, r});
+ while (bstrchr(client_msg, '\n') != -1) {
+ char *reply_msg = mp_ipc_consume_next_command(arg->client,
+ NULL, &client_msg);
+ if (reply_msg && arg->writable)
+ ipc_write_str(arg, reply_msg);
+ talloc_free(reply_msg);
+ }
+
+ // Begin the next read operation on the pipe
+ if ((ioerr = async_read(arg->client_h, buf, 4096, &ol))) {
+ report_read_error(arg, ioerr);
+ goto done;
+ }
+ break;
+ default:
+ MP_ERR(arg, "WaitForMultipleObjects failed\n");
+ goto done;
+ }
+ }
+
+done:
+ if (client_msg.len > 0)
+ MP_WARN(arg, "Ignoring unterminated command on disconnect.\n");
+
+ if (CancelIoEx(arg->client_h, &ol) || GetLastError() != ERROR_NOT_FOUND)
+ GetOverlappedResult(arg->client_h, &ol, &(DWORD){0}, TRUE);
+ if (wakeup_event)
+ CloseHandle(wakeup_event);
+ if (ol.hEvent)
+ CloseHandle(ol.hEvent);
+ if (arg->write_ol.hEvent)
+ CloseHandle(arg->write_ol.hEvent);
+
+ CloseHandle(arg->client_h);
+ mpv_destroy(arg->client);
+ talloc_free(arg);
+ MP_THREAD_RETURN();
+}
+
+static void ipc_start_client(struct mp_ipc_ctx *ctx, struct client_arg *client)
+{
+ client->client = mp_new_client(ctx->client_api, client->client_name),
+ client->log = mp_client_get_log(client->client);
+
+ mp_thread client_thr;
+ if (mp_thread_create(&client_thr, client_thread, client)) {
+ mpv_destroy(client->client);
+ CloseHandle(client->client_h);
+ talloc_free(client);
+ }
+ mp_thread_detach(client_thr);
+}
+
+static void ipc_start_client_json(struct mp_ipc_ctx *ctx, int id, HANDLE h)
+{
+ struct client_arg *client = talloc_ptrtype(NULL, client);
+ *client = (struct client_arg){
+ .client_name = talloc_asprintf(client, "ipc-%d", id),
+ .client_h = h,
+ .writable = true,
+ };
+
+ ipc_start_client(ctx, client);
+}
+
+bool mp_ipc_start_anon_client(struct mp_ipc_ctx *ctx, struct mpv_handle *h,
+ int out_fd[2])
+{
+ return false;
+}
+
+static MP_THREAD_VOID ipc_thread(void *p)
+{
+ // Use PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE so message framing is
+ // maintained for message-mode clients, but byte-mode clients can still
+ // connect, send and receive data. This is the most compatible mode.
+ static const DWORD state =
+ PIPE_TYPE_MESSAGE | PIPE_READMODE_BYTE | PIPE_WAIT |
+ PIPE_REJECT_REMOTE_CLIENTS;
+ static const DWORD mode =
+ PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED;
+ static const DWORD bufsiz = 4096;
+
+ struct mp_ipc_ctx *arg = p;
+ HANDLE server = INVALID_HANDLE_VALUE;
+ HANDLE client = INVALID_HANDLE_VALUE;
+ int client_num = 0;
+
+ mp_thread_set_name("ipc/named-pipe");
+ MP_VERBOSE(arg, "Starting IPC master\n");
+
+ OVERLAPPED ol = {0};
+ SECURITY_ATTRIBUTES sa = {
+ .nLength = sizeof sa,
+ .lpSecurityDescriptor = create_restricted_sd(),
+ };
+ if (!sa.lpSecurityDescriptor) {
+ MP_ERR(arg, "Couldn't create security descriptor");
+ goto done;
+ }
+
+ ol = (OVERLAPPED){ .hEvent = CreateEventW(NULL, TRUE, TRUE, NULL) };
+ if (!ol.hEvent) {
+ MP_ERR(arg, "Couldn't create event");
+ goto done;
+ }
+
+ server = CreateNamedPipeW(arg->path, mode | FILE_FLAG_FIRST_PIPE_INSTANCE,
+ state, PIPE_UNLIMITED_INSTANCES, bufsiz, bufsiz, 0, &sa);
+ if (server == INVALID_HANDLE_VALUE) {
+ MP_ERR(arg, "Couldn't create first pipe instance: %s\n",
+ mp_LastError_to_str());
+ goto done;
+ }
+
+ MP_VERBOSE(arg, "Listening to IPC pipe.\n");
+
+ while (1) {
+ DWORD err = ConnectNamedPipe(server, &ol) ? 0 : GetLastError();
+
+ if (err == ERROR_IO_PENDING) {
+ int n = WaitForMultipleObjects(2, (HANDLE[]) {
+ arg->death_event,
+ ol.hEvent,
+ }, FALSE, INFINITE) - WAIT_OBJECT_0;
+
+ switch (n) {
+ case 0:
+ // Stop waiting for new clients
+ CancelIo(server);
+ GetOverlappedResult(server, &ol, &(DWORD){0}, TRUE);
+ goto done;
+ case 1:
+ // Complete the ConnectNamedPipe request
+ err = GetOverlappedResult(server, &ol, &(DWORD){0}, TRUE)
+ ? 0 : GetLastError();
+ break;
+ default:
+ MP_ERR(arg, "WaitForMultipleObjects failed\n");
+ goto done;
+ }
+ }
+
+ // ERROR_PIPE_CONNECTED is returned if a client connects before
+ // ConnectNamedPipe is called. ERROR_NO_DATA is returned if a client
+ // connects, (possibly) writes data and exits before ConnectNamedPipe
+ // is called. Both cases should be handled as normal connections.
+ if (err == ERROR_PIPE_CONNECTED || err == ERROR_NO_DATA)
+ err = 0;
+
+ if (err) {
+ MP_ERR(arg, "ConnectNamedPipe failed: %s\n",
+ mp_HRESULT_to_str(HRESULT_FROM_WIN32(err)));
+ goto done;
+ }
+
+ // Create the next pipe instance before the client thread to avoid the
+ // theoretical race condition where the client thread immediately
+ // closes the handle and there are no active instances of the pipe
+ client = server;
+ server = CreateNamedPipeW(arg->path, mode, state,
+ PIPE_UNLIMITED_INSTANCES, bufsiz, bufsiz, 0, &sa);
+ if (server == INVALID_HANDLE_VALUE) {
+ MP_ERR(arg, "Couldn't create additional pipe instance: %s\n",
+ mp_LastError_to_str());
+ goto done;
+ }
+
+ ipc_start_client_json(arg, client_num++, client);
+ client = NULL;
+ }
+
+done:
+ if (sa.lpSecurityDescriptor)
+ LocalFree(sa.lpSecurityDescriptor);
+ if (client != INVALID_HANDLE_VALUE)
+ CloseHandle(client);
+ if (server != INVALID_HANDLE_VALUE)
+ CloseHandle(server);
+ if (ol.hEvent)
+ CloseHandle(ol.hEvent);
+ MP_THREAD_RETURN();
+}
+
+struct mp_ipc_ctx *mp_init_ipc(struct mp_client_api *client_api,
+ struct mpv_global *global)
+{
+ struct MPOpts *opts = mp_get_config_group(NULL, global, &mp_opt_root);
+
+ struct mp_ipc_ctx *arg = talloc_ptrtype(NULL, arg);
+ *arg = (struct mp_ipc_ctx){
+ .log = mp_log_new(arg, global->log, "ipc"),
+ .client_api = client_api,
+ };
+
+ if (!opts->ipc_path || !*opts->ipc_path)
+ goto out;
+
+ // Ensure the path is a legal Win32 pipe name by prepending \\.\pipe\ if
+ // it's not already present. Qt's QLocalSocket uses the same logic, so
+ // cross-platform programs that use paths like /tmp/mpv-socket should just
+ // work. (Win32 converts this path to \Device\NamedPipe\tmp\mpv-socket)
+ if (!strncmp(opts->ipc_path, "\\\\.\\pipe\\", 9)) {
+ arg->path = mp_from_utf8(arg, opts->ipc_path);
+ } else {
+ char *path = talloc_asprintf(NULL, "\\\\.\\pipe\\%s", opts->ipc_path);
+ arg->path = mp_from_utf8(arg, path);
+ talloc_free(path);
+ }
+
+ if (!(arg->death_event = CreateEventW(NULL, TRUE, FALSE, NULL)))
+ goto out;
+
+ if (mp_thread_create(&arg->thread, ipc_thread, arg))
+ goto out;
+
+ talloc_free(opts);
+ return arg;
+
+out:
+ if (arg->death_event)
+ CloseHandle(arg->death_event);
+ talloc_free(arg);
+ talloc_free(opts);
+ return NULL;
+}
+
+void mp_uninit_ipc(struct mp_ipc_ctx *arg)
+{
+ if (!arg)
+ return;
+
+ SetEvent(arg->death_event);
+ mp_thread_join(arg->thread);
+
+ CloseHandle(arg->death_event);
+ talloc_free(arg);
+}