mpv/input/ipc-win.c

529 lines
16 KiB
C

/*
* 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_client && opts->ipc_client[0]) {
int fd = -1;
if (strncmp(opts->ipc_client, "fd://", 5) == 0) {
char *end;
unsigned long l = strtoul(opts->ipc_client + 5, &end, 0);
if (!end[0] && l <= INT_MAX)
fd = l;
}
if (fd < 0) {
MP_ERR(arg, "Invalid IPC client argument: '%s'\n", opts->ipc_client);
} else {
HANDLE h = (HANDLE)_get_osfhandle(fd);
if (h && h != INVALID_HANDLE_VALUE && (intptr_t)h != -2)
ipc_start_client_json(arg, -1, h);
else
MP_ERR(arg, "Invalid IPC client fd: '%d'\n", fd);
}
}
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);
}