Add ctl control socket and initial command infrastructure
This implements the first wayvncctl command: set-output Signed-off-by: Jim Ramsay <i.am@jimramsay.com>pull/178/head
parent
19e1e14eab
commit
1a0e8aae97
|
@ -42,6 +42,7 @@ xbps-install wayvnc
|
||||||
* neatvnc
|
* neatvnc
|
||||||
* pam (optional)
|
* pam (optional)
|
||||||
* pixman
|
* pixman
|
||||||
|
* jansson
|
||||||
|
|
||||||
### Build Dependencies
|
### Build Dependencies
|
||||||
* GCC
|
* GCC
|
||||||
|
@ -51,7 +52,7 @@ xbps-install wayvnc
|
||||||
|
|
||||||
#### For Arch Linux
|
#### For Arch Linux
|
||||||
```
|
```
|
||||||
pacman -S base-devel libglvnd libxkbcommon pixman gnutls
|
pacman -S base-devel libglvnd libxkbcommon pixman gnutls jansson
|
||||||
```
|
```
|
||||||
|
|
||||||
#### For Fedora 31
|
#### For Fedora 31
|
||||||
|
@ -60,7 +61,7 @@ dnf install -y meson gcc ninja-build pkg-config egl-wayland egl-wayland-devel \
|
||||||
mesa-libEGL-devel mesa-libEGL libwayland-egl libglvnd-devel \
|
mesa-libEGL-devel mesa-libEGL libwayland-egl libglvnd-devel \
|
||||||
libglvnd-core-devel libglvnd mesa-libGLES-devel mesa-libGLES \
|
libglvnd-core-devel libglvnd mesa-libGLES-devel mesa-libGLES \
|
||||||
libxkbcommon-devel libxkbcommon libwayland-client libwayland \
|
libxkbcommon-devel libxkbcommon libwayland-client libwayland \
|
||||||
wayland-devel gnutls-devel
|
wayland-devel gnutls-devel jansson-devel
|
||||||
```
|
```
|
||||||
|
|
||||||
#### For Debian (unstable / testing)
|
#### For Debian (unstable / testing)
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Jim Ramsay
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||||
|
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
* PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "output.h"
|
||||||
|
#include <wayland-client.h>
|
||||||
|
|
||||||
|
struct ctl;
|
||||||
|
struct cmd_response;
|
||||||
|
|
||||||
|
struct ctl_server_actions {
|
||||||
|
void* userdata;
|
||||||
|
struct cmd_response* (*on_output_cycle)(struct ctl*,
|
||||||
|
enum output_cycle_direction direction);
|
||||||
|
struct cmd_response* (*on_output_switch)(struct ctl*,
|
||||||
|
const char* output_name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* default_ctl_socket_path();
|
||||||
|
|
||||||
|
struct ctl* ctl_server_new(const char* socket_path,
|
||||||
|
const struct ctl_server_actions* actions);
|
||||||
|
void ctl_server_destroy(struct ctl*);
|
||||||
|
void* ctl_server_userdata(struct ctl*);
|
||||||
|
|
||||||
|
struct cmd_response* cmd_ok(void);
|
||||||
|
struct cmd_response* cmd_failed(const char* fmt, ...);
|
|
@ -91,6 +91,7 @@ sources = [
|
||||||
'src/pixels.c',
|
'src/pixels.c',
|
||||||
'src/transform-util.c',
|
'src/transform-util.c',
|
||||||
'src/json-ipc.c',
|
'src/json-ipc.c',
|
||||||
|
'src/ctl-server.c',
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
|
@ -0,0 +1,625 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Jim Ramsay
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||||
|
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
* PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <neatvnc.h>
|
||||||
|
#include <aml.h>
|
||||||
|
#include <jansson.h>
|
||||||
|
|
||||||
|
#include "output.h"
|
||||||
|
#include "ctl-server.h"
|
||||||
|
#include "json-ipc.h"
|
||||||
|
#include "util.h"
|
||||||
|
#include "strlcpy.h"
|
||||||
|
|
||||||
|
#define FAILED_TO(action) \
|
||||||
|
nvnc_log(NVNC_LOG_WARNING, "Failed to " action ": %m");
|
||||||
|
|
||||||
|
enum cmd_type {
|
||||||
|
CMD_SET_OUTPUT,
|
||||||
|
CMD_UNKNOWN,
|
||||||
|
};
|
||||||
|
|
||||||
|
static char* cmd_name[] = {
|
||||||
|
[CMD_SET_OUTPUT] = "set-output",
|
||||||
|
};
|
||||||
|
|
||||||
|
struct cmd {
|
||||||
|
enum cmd_type type;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct cmd_set_output {
|
||||||
|
struct cmd cmd;
|
||||||
|
char target[64];
|
||||||
|
enum output_cycle_direction cycle;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct cmd_response {
|
||||||
|
int code;
|
||||||
|
json_t* data;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ctl_client {
|
||||||
|
int fd;
|
||||||
|
struct wl_list link;
|
||||||
|
struct ctl* server;
|
||||||
|
struct aml_handler* handler;
|
||||||
|
char read_buffer[512];
|
||||||
|
size_t read_len;
|
||||||
|
json_t* response_queue;
|
||||||
|
char* write_buffer;
|
||||||
|
char* write_ptr;
|
||||||
|
size_t write_len;
|
||||||
|
bool drop_after_next_send;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ctl {
|
||||||
|
char socket_path[255];
|
||||||
|
struct ctl_server_actions actions;
|
||||||
|
int fd;
|
||||||
|
struct aml_handler* handler;
|
||||||
|
struct wl_list clients;
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct cmd_response* cmd_response_new(int code, json_t* data)
|
||||||
|
{
|
||||||
|
struct cmd_response* new = calloc(1, sizeof(struct cmd_response));
|
||||||
|
new->code = code;
|
||||||
|
new->data = data;
|
||||||
|
return new;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cmd_response_destroy(struct cmd_response* self)
|
||||||
|
{
|
||||||
|
json_decref(self->data);
|
||||||
|
free(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
static enum cmd_type parse_command_name(const char* name)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < CMD_UNKNOWN; ++i) {
|
||||||
|
if (strcmp(name, cmd_name[i]) == 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CMD_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct cmd_set_output* cmd_set_output_new(json_t* args,
|
||||||
|
struct jsonipc_error* err)
|
||||||
|
{
|
||||||
|
const char* target = NULL;
|
||||||
|
const char* cycle = NULL;
|
||||||
|
if (json_unpack(args, "{s?s,s?s}",
|
||||||
|
"switch-to", &target,
|
||||||
|
"cycle", &cycle) == -1) {
|
||||||
|
jsonipc_error_printf(err, EINVAL,
|
||||||
|
"expecting \"switch-to\" or \"cycle\"");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if ((!target && !cycle) || (target && cycle)) {
|
||||||
|
jsonipc_error_printf(err, EINVAL,
|
||||||
|
"expecting exactly one of \"switch-to\" or \"cycle\"");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
struct cmd_set_output* cmd = calloc(1, sizeof(*cmd));
|
||||||
|
cmd->cmd.type = CMD_SET_OUTPUT;
|
||||||
|
if (target) {
|
||||||
|
strlcpy(cmd->target, target, sizeof(cmd->target));
|
||||||
|
} else if (cycle) {
|
||||||
|
if (strncmp(cycle, "prev", 4) == 0)
|
||||||
|
cmd->cycle = OUTPUT_CYCLE_REVERSE;
|
||||||
|
else if (strcmp(cycle, "next") == 0)
|
||||||
|
cmd->cycle = OUTPUT_CYCLE_FORWARD;
|
||||||
|
else {
|
||||||
|
jsonipc_error_printf(err, EINVAL,
|
||||||
|
"cycle must either be \"next\" or \"prev\"");
|
||||||
|
free(cmd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct cmd* parse_command(struct jsonipc_request* ipc,
|
||||||
|
struct jsonipc_error* err)
|
||||||
|
{
|
||||||
|
nvnc_trace("Parsing command %s", ipc->method);
|
||||||
|
enum cmd_type cmd_type = parse_command_name(ipc->method);
|
||||||
|
struct cmd* cmd = NULL;
|
||||||
|
switch (cmd_type) {
|
||||||
|
case CMD_SET_OUTPUT:
|
||||||
|
cmd = (struct cmd*)cmd_set_output_new(ipc->params, err);
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
json_t* allowed = json_array();
|
||||||
|
for (int i = 0; i < CMD_UNKNOWN; ++i)
|
||||||
|
json_array_append_new(allowed, json_string(cmd_name[i]));
|
||||||
|
jsonipc_error_set_new(err, ENOENT,
|
||||||
|
json_pack("{s:o, s:o}",
|
||||||
|
"error",
|
||||||
|
jprintf("Unknown command \"%s\"",
|
||||||
|
ipc->method),
|
||||||
|
"commands", allowed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void client_destroy(struct ctl_client* self)
|
||||||
|
{
|
||||||
|
nvnc_trace("Destroying client %p", self);
|
||||||
|
aml_stop(aml_get_default(), self->handler);
|
||||||
|
aml_unref(self->handler);
|
||||||
|
close(self->fd);
|
||||||
|
json_array_clear(self->response_queue);
|
||||||
|
json_decref(self->response_queue);
|
||||||
|
wl_list_remove(&self->link);
|
||||||
|
free(self);
|
||||||
|
}
|
||||||
|
static void set_internal_error(struct cmd_response** err, int code,
|
||||||
|
const char* fmt, ...)
|
||||||
|
{
|
||||||
|
char msg[256];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vsnprintf(msg, sizeof(msg), fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
nvnc_log(NVNC_LOG_WARNING, msg);
|
||||||
|
*err = cmd_response_new(code, json_pack("{s:s}", "error", msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return values:
|
||||||
|
// >0: Number of bytes read
|
||||||
|
// 0: No bytes read (EAGAIN)
|
||||||
|
// -1: Fatal error. Check 'err' for details, or if 'err' is null, terminate the connection.
|
||||||
|
static ssize_t client_read(struct ctl_client* self, struct cmd_response** err)
|
||||||
|
{
|
||||||
|
size_t bufferspace = sizeof(self->read_buffer) - self->read_len;
|
||||||
|
if (bufferspace == 0) {
|
||||||
|
set_internal_error(err, EIO, "Buffer overflow");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
ssize_t n = recv(self->fd, self->read_buffer + self->read_len, bufferspace,
|
||||||
|
MSG_DONTWAIT);
|
||||||
|
if (n == -1) {
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||||
|
nvnc_trace("recv: EAGAIN");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
set_internal_error(err, EIO, "Read failed: %m");
|
||||||
|
return -1;
|
||||||
|
} else if (n == 0) {
|
||||||
|
nvnc_log(NVNC_LOG_INFO, "Control socket client disconnected: %p", self);
|
||||||
|
errno = ENOTCONN;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->read_len += n;
|
||||||
|
nvnc_trace("Read %d bytes, total is now %d", n, self->read_len);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void client_advance_buffer(struct ctl_client* self, size_t len)
|
||||||
|
{
|
||||||
|
size_t remainder = self->read_len - len;
|
||||||
|
if (remainder > 0)
|
||||||
|
memmove(self->read_buffer, self->read_buffer + len, remainder);
|
||||||
|
self->read_len = remainder;
|
||||||
|
}
|
||||||
|
|
||||||
|
static json_t* client_next_object(struct ctl_client* self,
|
||||||
|
struct cmd_response** ierr)
|
||||||
|
{
|
||||||
|
if (self->read_len == 0)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
json_error_t err;
|
||||||
|
json_t* root = json_loadb(self->read_buffer, self->read_len,
|
||||||
|
JSON_DISABLE_EOF_CHECK, &err);
|
||||||
|
if (root) {
|
||||||
|
nvnc_log(NVNC_LOG_DEBUG, "<< %.*s", err.position, self->read_buffer);
|
||||||
|
client_advance_buffer(self, err.position);
|
||||||
|
} else if (json_error_code(&err) == json_error_premature_end_of_input) {
|
||||||
|
nvnc_trace("Awaiting more data");
|
||||||
|
} else {
|
||||||
|
set_internal_error(ierr, EINVAL, err.text);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct cmd_response* ctl_server_dispatch_cmd(struct ctl* self, struct cmd* cmd)
|
||||||
|
{
|
||||||
|
assert(cmd->type != CMD_UNKNOWN);
|
||||||
|
const char* name = cmd_name[cmd->type];
|
||||||
|
nvnc_log(NVNC_LOG_INFO, "Dispatching control client command '%s'", name);
|
||||||
|
struct cmd_response* response = NULL;
|
||||||
|
switch (cmd->type) {
|
||||||
|
case CMD_SET_OUTPUT: {
|
||||||
|
struct cmd_set_output* c = (struct cmd_set_output*)cmd;
|
||||||
|
if (c->target[0] != '\0')
|
||||||
|
response = self->actions.on_output_switch(self, c->target);
|
||||||
|
else
|
||||||
|
response = self->actions.on_output_cycle(self, c->cycle);
|
||||||
|
}
|
||||||
|
case CMD_UNKNOWN:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void client_set_aml_event_mask(struct ctl_client* self)
|
||||||
|
{
|
||||||
|
int mask = AML_EVENT_READ;
|
||||||
|
if (json_array_size(self->response_queue) > 0 ||
|
||||||
|
self->write_len)
|
||||||
|
mask |= AML_EVENT_WRITE;
|
||||||
|
aml_set_event_mask(self->handler, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int client_enqueue_jsonipc(struct ctl_client* self,
|
||||||
|
struct jsonipc_response* resp)
|
||||||
|
{
|
||||||
|
int result = 0;
|
||||||
|
json_error_t err;
|
||||||
|
json_t* packed_response = jsonipc_response_pack(resp, &err);
|
||||||
|
if (!packed_response) {
|
||||||
|
nvnc_log(NVNC_LOG_WARNING, "Pack failed: %s", err.text);
|
||||||
|
result = -1;
|
||||||
|
goto failure;
|
||||||
|
}
|
||||||
|
result = json_array_append_new(self->response_queue, packed_response);
|
||||||
|
if (result != 0) {
|
||||||
|
nvnc_log(NVNC_LOG_WARNING, "Append failed");
|
||||||
|
goto failure;
|
||||||
|
}
|
||||||
|
client_set_aml_event_mask(self);
|
||||||
|
failure:
|
||||||
|
jsonipc_response_destroy(resp);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int client_enqueue_error(struct ctl_client* self,
|
||||||
|
struct jsonipc_error* err, json_t* id)
|
||||||
|
{
|
||||||
|
struct jsonipc_response* resp = jsonipc_error_response_new(err, id);
|
||||||
|
return client_enqueue_jsonipc(self, resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int client_enqueue_response(struct ctl_client* self,
|
||||||
|
struct cmd_response* response, json_t* id)
|
||||||
|
{
|
||||||
|
nvnc_log(NVNC_LOG_INFO, "Enqueueing response: %s (%d)",
|
||||||
|
response->code == 0 ? "OK" : "FAILED", response->code);
|
||||||
|
char* str = NULL;
|
||||||
|
if (response->data)
|
||||||
|
str = json_dumps(response->data, 0);
|
||||||
|
nvnc_log(NVNC_LOG_DEBUG, "Response data: %s", str);
|
||||||
|
if(str)
|
||||||
|
free(str);
|
||||||
|
struct jsonipc_response* resp =
|
||||||
|
jsonipc_response_new(response->code, response->data, id);
|
||||||
|
cmd_response_destroy(response);
|
||||||
|
return client_enqueue_jsonipc(self, resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int client_enqueue_internal_error(struct ctl_client* self,
|
||||||
|
struct cmd_response* err)
|
||||||
|
{
|
||||||
|
int result = client_enqueue_response(self, err, NULL);
|
||||||
|
if (result != 0)
|
||||||
|
client_destroy(self);
|
||||||
|
self->drop_after_next_send = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_ready(struct ctl_client* client)
|
||||||
|
{
|
||||||
|
if (client->write_buffer) {
|
||||||
|
nvnc_trace("Continuing partial write (%d left)", client->write_len);
|
||||||
|
} else if (json_array_size(client->response_queue) > 0){
|
||||||
|
nvnc_trace("Sending new queued message");
|
||||||
|
json_t* item = json_array_get(client->response_queue, 0);
|
||||||
|
client->write_len = json_dumpb(item, NULL, 0, JSON_COMPACT);
|
||||||
|
client->write_buffer = calloc(1, client->write_len);
|
||||||
|
client->write_ptr = client->write_buffer;
|
||||||
|
json_dumpb(item, client->write_buffer, client->write_len,
|
||||||
|
JSON_COMPACT);
|
||||||
|
nvnc_log(NVNC_LOG_DEBUG, ">> %.*s", client->write_len, client->write_buffer);
|
||||||
|
json_array_remove(client->response_queue, 0);
|
||||||
|
} else {
|
||||||
|
nvnc_trace("Nothing to send");
|
||||||
|
}
|
||||||
|
if (!client->write_ptr)
|
||||||
|
goto no_data;
|
||||||
|
ssize_t n = send(client->fd, client->write_ptr, client->write_len,
|
||||||
|
MSG_NOSIGNAL|MSG_DONTWAIT);
|
||||||
|
if (n == -1) {
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||||
|
nvnc_trace("send: EAGAIN");
|
||||||
|
goto send_eagain;
|
||||||
|
}
|
||||||
|
nvnc_log(NVNC_LOG_ERROR, "Could not send response: %m");
|
||||||
|
client_destroy(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nvnc_trace("sent %d/%d bytes", n, client->write_len);
|
||||||
|
client->write_ptr += n;
|
||||||
|
client->write_len -= n;
|
||||||
|
send_eagain:
|
||||||
|
if (client->write_len == 0) {
|
||||||
|
nvnc_trace("Write buffer empty!");
|
||||||
|
free(client->write_buffer);
|
||||||
|
client->write_buffer = NULL;
|
||||||
|
client->write_ptr = NULL;
|
||||||
|
if (client->drop_after_next_send) {
|
||||||
|
nvnc_log(NVNC_LOG_WARNING, "Intentional disconnect");
|
||||||
|
client_destroy(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nvnc_trace("Write buffer has %d remaining", client->write_len);
|
||||||
|
}
|
||||||
|
no_data:
|
||||||
|
client_set_aml_event_mask(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void recv_ready(struct ctl_client* client)
|
||||||
|
{
|
||||||
|
struct ctl* server = client->server;
|
||||||
|
struct cmd_response* details = NULL;
|
||||||
|
switch (client_read(client, &details)) {
|
||||||
|
case 0: // Needs more data
|
||||||
|
return;
|
||||||
|
case -1: // Fatal error
|
||||||
|
if (details)
|
||||||
|
client_enqueue_internal_error(client, details);
|
||||||
|
else
|
||||||
|
client_destroy(client);
|
||||||
|
return;
|
||||||
|
default: // Read some data; check it
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
json_t* root;
|
||||||
|
while (true) {
|
||||||
|
root = client_next_object(client, &details);
|
||||||
|
if (root == NULL)
|
||||||
|
break;
|
||||||
|
|
||||||
|
struct jsonipc_error jipc_err = JSONIPC_ERR_INIT;
|
||||||
|
|
||||||
|
struct jsonipc_request* request =
|
||||||
|
jsonipc_request_parse_new(root, &jipc_err);
|
||||||
|
if (!request) {
|
||||||
|
client_enqueue_error(client, &jipc_err,
|
||||||
|
NULL);
|
||||||
|
goto request_parse_failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct cmd* cmd = parse_command(request, &jipc_err);
|
||||||
|
if (!cmd) {
|
||||||
|
client_enqueue_error(client, &jipc_err,
|
||||||
|
request->id);
|
||||||
|
goto cmdparse_failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Enqueue the command (and request ID) to be
|
||||||
|
// handled by the main loop instead of doing the
|
||||||
|
// dispatch here
|
||||||
|
struct cmd_response* response =
|
||||||
|
ctl_server_dispatch_cmd(server, cmd);
|
||||||
|
if (!response)
|
||||||
|
goto no_response;
|
||||||
|
client_enqueue_response(client, response, request->id);
|
||||||
|
no_response:
|
||||||
|
free(cmd);
|
||||||
|
cmdparse_failed:
|
||||||
|
jsonipc_request_destroy(request);
|
||||||
|
request_parse_failed:
|
||||||
|
jsonipc_error_cleanup(&jipc_err);
|
||||||
|
json_decref(root);
|
||||||
|
}
|
||||||
|
if (details)
|
||||||
|
client_enqueue_internal_error(client, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_ready(void* obj)
|
||||||
|
{
|
||||||
|
struct ctl_client* client = aml_get_userdata(obj);
|
||||||
|
uint32_t events = aml_get_revents(obj);
|
||||||
|
nvnc_trace("Client %p ready: 0x%x", client, events);
|
||||||
|
|
||||||
|
if (events & AML_EVENT_WRITE)
|
||||||
|
send_ready(client);
|
||||||
|
else if (events & AML_EVENT_READ)
|
||||||
|
recv_ready(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_connection(void* obj)
|
||||||
|
{
|
||||||
|
nvnc_log(NVNC_LOG_DEBUG, "New connection");
|
||||||
|
struct ctl* server = aml_get_userdata(obj);
|
||||||
|
|
||||||
|
struct ctl_client* client = calloc(1, sizeof(*client));
|
||||||
|
if (!client) {
|
||||||
|
FAILED_TO("allocate a client object");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client->server = server;
|
||||||
|
client->response_queue = json_array();
|
||||||
|
|
||||||
|
client->fd = accept(server->fd, NULL, 0);
|
||||||
|
if (client->fd < 0) {
|
||||||
|
FAILED_TO("accept a connection");
|
||||||
|
goto accept_failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
client->handler = aml_handler_new(client->fd, on_ready, client, NULL);
|
||||||
|
if (!client->handler) {
|
||||||
|
FAILED_TO("create a loop handler");
|
||||||
|
goto handle_failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aml_start(aml_get_default(), client->handler) < 0) {
|
||||||
|
FAILED_TO("register for client events");
|
||||||
|
goto poll_start_failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
wl_list_insert(&server->clients, &client->link);
|
||||||
|
nvnc_log(NVNC_LOG_INFO, "New control socket client connected: %p", client);
|
||||||
|
return;
|
||||||
|
|
||||||
|
poll_start_failure:
|
||||||
|
aml_unref(client->handler);
|
||||||
|
handle_failure:
|
||||||
|
close(client->fd);
|
||||||
|
accept_failure:
|
||||||
|
json_decref(client->response_queue);
|
||||||
|
free(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* default_ctl_socket_path()
|
||||||
|
{
|
||||||
|
static char buffer[128];
|
||||||
|
char* xdg_runtime = getenv("XDG_RUNTIME_DIR");
|
||||||
|
if (xdg_runtime)
|
||||||
|
snprintf(buffer, sizeof(buffer),
|
||||||
|
"%s/wayvncctl", xdg_runtime);
|
||||||
|
else
|
||||||
|
snprintf(buffer, sizeof(buffer),
|
||||||
|
"/tmp/wayvncctl-%d", getuid());
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ctl_server_init(struct ctl* self, const char* socket_path)
|
||||||
|
{
|
||||||
|
if (!socket_path) {
|
||||||
|
socket_path = default_ctl_socket_path();
|
||||||
|
if (!getenv("XDG_RUNTIME_DIR"))
|
||||||
|
nvnc_log(NVNC_LOG_WARNING, "$XDG_RUNTIME_DIR is not set. Falling back to control socket \"%s\"", socket_path);
|
||||||
|
}
|
||||||
|
strlcpy(self->socket_path, socket_path, sizeof(self->socket_path));
|
||||||
|
nvnc_log(NVNC_LOG_DEBUG, "Initializing wayvncctl socket: %s", self->socket_path);
|
||||||
|
|
||||||
|
wl_list_init(&self->clients);
|
||||||
|
|
||||||
|
struct sockaddr_un addr = {
|
||||||
|
.sun_family = AF_UNIX,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (strlen(self->socket_path) >= sizeof(addr.sun_path)) {
|
||||||
|
errno = ENAMETOOLONG;
|
||||||
|
FAILED_TO("create unix socket");
|
||||||
|
goto socket_failure;
|
||||||
|
}
|
||||||
|
strcpy(addr.sun_path, self->socket_path);
|
||||||
|
|
||||||
|
self->fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||||
|
if (self->fd < 0) {
|
||||||
|
FAILED_TO("create unix socket");
|
||||||
|
goto socket_failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bind(self->fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
|
||||||
|
FAILED_TO("bind unix socket");
|
||||||
|
goto bind_failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen(self->fd, 16) < 0) {
|
||||||
|
FAILED_TO("listen to unix socket");
|
||||||
|
goto listen_failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->handler = aml_handler_new(self->fd, on_connection, self, NULL);
|
||||||
|
if (!self->handler) {
|
||||||
|
FAILED_TO("create a main loop handler");
|
||||||
|
goto handle_failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aml_start(aml_get_default(), self->handler) < 0) {
|
||||||
|
FAILED_TO("Register for server events");
|
||||||
|
goto poll_start_failure;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
poll_start_failure:
|
||||||
|
aml_unref(self->handler);
|
||||||
|
handle_failure:
|
||||||
|
listen_failure:
|
||||||
|
close(self->fd);
|
||||||
|
unlink(self->socket_path);
|
||||||
|
bind_failure:
|
||||||
|
socket_failure:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ctl_server_stop(struct ctl* self)
|
||||||
|
{
|
||||||
|
aml_stop(aml_get_default(), self->handler);
|
||||||
|
aml_unref(self->handler);
|
||||||
|
struct ctl_client* client;
|
||||||
|
struct ctl_client* tmp;
|
||||||
|
wl_list_for_each_safe(client, tmp, &self->clients, link)
|
||||||
|
client_destroy(client);
|
||||||
|
close(self->fd);
|
||||||
|
unlink(self->socket_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ctl* ctl_server_new(const char* socket_path,
|
||||||
|
const struct ctl_server_actions* actions)
|
||||||
|
{
|
||||||
|
struct ctl* ctl = calloc(1, sizeof(*ctl));
|
||||||
|
memcpy(&ctl->actions, actions, sizeof(*actions));
|
||||||
|
if (ctl_server_init(ctl, socket_path) != 0) {
|
||||||
|
free(ctl);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return ctl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ctl_server_destroy(struct ctl* self)
|
||||||
|
{
|
||||||
|
ctl_server_stop(self);
|
||||||
|
free(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
void* ctl_server_userdata(struct ctl* self)
|
||||||
|
{
|
||||||
|
return self->actions.userdata;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct cmd_response* cmd_ok()
|
||||||
|
{
|
||||||
|
return cmd_response_new(0, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct cmd_response* cmd_failed(const char* fmt, ...)
|
||||||
|
{
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
struct cmd_response* resp = cmd_response_new(1, json_pack("{s:o}",
|
||||||
|
"error", jvprintf(fmt, ap)));
|
||||||
|
va_end(ap);
|
||||||
|
return resp;
|
||||||
|
}
|
53
src/main.c
53
src/main.c
|
@ -50,6 +50,7 @@
|
||||||
#include "cfg.h"
|
#include "cfg.h"
|
||||||
#include "transform-util.h"
|
#include "transform-util.h"
|
||||||
#include "usdt.h"
|
#include "usdt.h"
|
||||||
|
#include "ctl-server.h"
|
||||||
|
|
||||||
#ifdef ENABLE_PAM
|
#ifdef ENABLE_PAM
|
||||||
#include "pam_auth.h"
|
#include "pam_auth.h"
|
||||||
|
@ -102,11 +103,14 @@ struct wayvnc {
|
||||||
|
|
||||||
int nr_clients;
|
int nr_clients;
|
||||||
struct aml_ticker* performance_ticker;
|
struct aml_ticker* performance_ticker;
|
||||||
|
|
||||||
|
struct ctl* ctl;
|
||||||
};
|
};
|
||||||
|
|
||||||
void wayvnc_exit(struct wayvnc* self);
|
void wayvnc_exit(struct wayvnc* self);
|
||||||
void on_capture_done(struct screencopy* sc);
|
void on_capture_done(struct screencopy* sc);
|
||||||
static void on_client_new(struct nvnc_client* client);
|
static void on_client_new(struct nvnc_client* client);
|
||||||
|
void switch_to_output(struct wayvnc*, struct output*);
|
||||||
void switch_to_next_output(struct wayvnc*);
|
void switch_to_next_output(struct wayvnc*);
|
||||||
void switch_to_prev_output(struct wayvnc*);
|
void switch_to_prev_output(struct wayvnc*);
|
||||||
|
|
||||||
|
@ -438,6 +442,32 @@ void on_signal(void* obj)
|
||||||
wayvnc_exit(self);
|
wayvnc_exit(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct cmd_response* on_output_cycle(struct ctl* ctl, enum output_cycle_direction direction)
|
||||||
|
{
|
||||||
|
struct wayvnc* self = ctl_server_userdata(ctl);
|
||||||
|
nvnc_log(NVNC_LOG_INFO, "ctl command: Rotating to %s output",
|
||||||
|
direction == OUTPUT_CYCLE_FORWARD ? "next" : "previous");
|
||||||
|
struct output* next = output_cycle(&self->outputs,
|
||||||
|
self->selected_output, direction);
|
||||||
|
switch_to_output(self, next);
|
||||||
|
return cmd_ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct cmd_response* on_output_switch(struct ctl* ctl,
|
||||||
|
const char* output_name)
|
||||||
|
{
|
||||||
|
nvnc_log(NVNC_LOG_INFO, "ctl command: Switch to output \"%s\"", output_name);
|
||||||
|
struct wayvnc* self = ctl_server_userdata(ctl);
|
||||||
|
if (!output_name || output_name[0] == '\0')
|
||||||
|
return cmd_failed("Output name is required");
|
||||||
|
struct output* output = output_find_by_name(&self->outputs, output_name);
|
||||||
|
if (!output) {
|
||||||
|
return cmd_failed("No such output \"%s\"", output_name);
|
||||||
|
}
|
||||||
|
switch_to_output(self, output);
|
||||||
|
return cmd_ok();
|
||||||
|
}
|
||||||
|
|
||||||
int init_main_loop(struct wayvnc* self)
|
int init_main_loop(struct wayvnc* self)
|
||||||
{
|
{
|
||||||
struct aml* loop = aml_get_default();
|
struct aml* loop = aml_get_default();
|
||||||
|
@ -726,6 +756,8 @@ int wayvnc_usage(FILE* stream, int rc)
|
||||||
" -k,--keyboard=<layout>[-<variant>] Select keyboard layout with an\n"
|
" -k,--keyboard=<layout>[-<variant>] Select keyboard layout with an\n"
|
||||||
" optional variant.\n"
|
" optional variant.\n"
|
||||||
" -s,--seat=<name> Select seat by name.\n"
|
" -s,--seat=<name> Select seat by name.\n"
|
||||||
|
" -S,--socket=<path> Wayvnc control socket path.\n"
|
||||||
|
" Default: $XDG_RUNTIME_DIR/wayvncctl\n"
|
||||||
" -r,--render-cursor Enable overlay cursor rendering.\n"
|
" -r,--render-cursor Enable overlay cursor rendering.\n"
|
||||||
" -f,--max-fps=<fps> Set the rate limit (default 30).\n"
|
" -f,--max-fps=<fps> Set the rate limit (default 30).\n"
|
||||||
" -p,--show-performance Show performance counters.\n"
|
" -p,--show-performance Show performance counters.\n"
|
||||||
|
@ -905,7 +937,7 @@ void set_selected_output(struct wayvnc* self, struct output* output) {
|
||||||
void switch_to_output(struct wayvnc* self, struct output* output)
|
void switch_to_output(struct wayvnc* self, struct output* output)
|
||||||
{
|
{
|
||||||
if (self->selected_output == output) {
|
if (self->selected_output == output) {
|
||||||
nvnc_log(NVNC_LOG_DEBUG, "No-op: Already selected output %s",
|
nvnc_log(NVNC_LOG_INFO, "Already selected output %s",
|
||||||
output->name);
|
output->name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -920,6 +952,7 @@ void switch_to_next_output(struct wayvnc* self)
|
||||||
nvnc_log(NVNC_LOG_INFO, "Rotating to next output");
|
nvnc_log(NVNC_LOG_INFO, "Rotating to next output");
|
||||||
struct output* next = output_cycle(&self->outputs,
|
struct output* next = output_cycle(&self->outputs,
|
||||||
self->selected_output, OUTPUT_CYCLE_FORWARD);
|
self->selected_output, OUTPUT_CYCLE_FORWARD);
|
||||||
|
|
||||||
switch_to_output(self, next);
|
switch_to_output(self, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -963,13 +996,14 @@ int main(int argc, char* argv[])
|
||||||
|
|
||||||
const char* output_name = NULL;
|
const char* output_name = NULL;
|
||||||
const char* seat_name = NULL;
|
const char* seat_name = NULL;
|
||||||
|
const char* socket_path = NULL;
|
||||||
|
|
||||||
bool overlay_cursor = false;
|
bool overlay_cursor = false;
|
||||||
bool show_performance = false;
|
bool show_performance = false;
|
||||||
int max_rate = 30;
|
int max_rate = 30;
|
||||||
bool disable_input = false;
|
bool disable_input = false;
|
||||||
|
|
||||||
static const char* shortopts = "C:go:k:s:rf:hpudVvL:";
|
static const char* shortopts = "C:go:k:s:S:rf:hpudVvL:";
|
||||||
int drm_fd MAYBE_UNUSED = -1;
|
int drm_fd MAYBE_UNUSED = -1;
|
||||||
|
|
||||||
int log_level = NVNC_LOG_WARNING;
|
int log_level = NVNC_LOG_WARNING;
|
||||||
|
@ -980,6 +1014,7 @@ int main(int argc, char* argv[])
|
||||||
{ "output", required_argument, NULL, 'o' },
|
{ "output", required_argument, NULL, 'o' },
|
||||||
{ "keyboard", required_argument, NULL, 'k' },
|
{ "keyboard", required_argument, NULL, 'k' },
|
||||||
{ "seat", required_argument, NULL, 's' },
|
{ "seat", required_argument, NULL, 's' },
|
||||||
|
{ "socket", required_argument, NULL, 'S' },
|
||||||
{ "render-cursor", no_argument, NULL, 'r' },
|
{ "render-cursor", no_argument, NULL, 'r' },
|
||||||
{ "max-fps", required_argument, NULL, 'f' },
|
{ "max-fps", required_argument, NULL, 'f' },
|
||||||
{ "help", no_argument, NULL, 'h' },
|
{ "help", no_argument, NULL, 'h' },
|
||||||
|
@ -1013,6 +1048,9 @@ int main(int argc, char* argv[])
|
||||||
case 's':
|
case 's':
|
||||||
seat_name = optarg;
|
seat_name = optarg;
|
||||||
break;
|
break;
|
||||||
|
case 'S':
|
||||||
|
socket_path = optarg;
|
||||||
|
break;
|
||||||
case 'r':
|
case 'r':
|
||||||
overlay_cursor = true;
|
overlay_cursor = true;
|
||||||
break;
|
break;
|
||||||
|
@ -1191,6 +1229,15 @@ int main(int argc, char* argv[])
|
||||||
self.performance_ticker = aml_ticker_new(1000000, on_perf_tick,
|
self.performance_ticker = aml_ticker_new(1000000, on_perf_tick,
|
||||||
&self, NULL);
|
&self, NULL);
|
||||||
|
|
||||||
|
const struct ctl_server_actions ctl_actions = {
|
||||||
|
.userdata = &self,
|
||||||
|
.on_output_cycle = on_output_cycle,
|
||||||
|
.on_output_switch = on_output_switch,
|
||||||
|
};
|
||||||
|
self.ctl = ctl_server_new(socket_path, &ctl_actions);
|
||||||
|
if (!self.ctl)
|
||||||
|
goto ctl_server_failure;
|
||||||
|
|
||||||
wl_display_dispatch_pending(self.display);
|
wl_display_dispatch_pending(self.display);
|
||||||
|
|
||||||
while (!self.do_exit) {
|
while (!self.do_exit) {
|
||||||
|
@ -1202,6 +1249,7 @@ int main(int argc, char* argv[])
|
||||||
nvnc_log(NVNC_LOG_INFO, "Exiting...");
|
nvnc_log(NVNC_LOG_INFO, "Exiting...");
|
||||||
|
|
||||||
screencopy_stop(&self.screencopy);
|
screencopy_stop(&self.screencopy);
|
||||||
|
ctl_server_destroy(self.ctl);
|
||||||
|
|
||||||
nvnc_display_unref(self.nvnc_display);
|
nvnc_display_unref(self.nvnc_display);
|
||||||
nvnc_close(self.nvnc);
|
nvnc_close(self.nvnc);
|
||||||
|
@ -1222,6 +1270,7 @@ int main(int argc, char* argv[])
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
ctl_server_failure:
|
||||||
capture_failure:
|
capture_failure:
|
||||||
nvnc_display_unref(self.nvnc_display);
|
nvnc_display_unref(self.nvnc_display);
|
||||||
nvnc_close(self.nvnc);
|
nvnc_close(self.nvnc);
|
||||||
|
|
Loading…
Reference in New Issue