Add server event infrastructure

Includes "client-connect" and "client-disconnect" events as
proof-of-concept.

Signed-off-by: Jim Ramsay <i.am@jimramsay.com>
pull/183/head
Jim Ramsay 2022-11-06 15:05:14 -05:00 committed by Andri Yngvason
parent 8d32dfaead
commit c75b64eae8
6 changed files with 241 additions and 4 deletions

View File

@ -37,3 +37,15 @@ void* ctl_server_userdata(struct ctl*);
struct cmd_response* cmd_ok(void); struct cmd_response* cmd_ok(void);
struct cmd_response* cmd_failed(const char* fmt, ...); struct cmd_response* cmd_failed(const char* fmt, ...);
void ctl_server_event_connected(struct ctl*,
const char* client_id,
const char* client_hostname,
const char* client_username,
int new_connection_count);
void ctl_server_event_disconnected(struct ctl*,
const char* client_id,
const char* client_hostname,
const char* client_username,
int new_connection_count);

View File

@ -21,6 +21,7 @@
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/un.h> #include <sys/un.h>
#include <poll.h> #include <poll.h>
#include <signal.h>
#include <jansson.h> #include <jansson.h>
#include "json-ipc.h" #include "json-ipc.h"
@ -46,6 +47,8 @@ struct ctl_client {
char read_buffer[512]; char read_buffer[512];
size_t read_len; size_t read_len;
bool wait_for_events;
int fd; int fd;
}; };
@ -197,8 +200,6 @@ static json_t* read_one_object(struct ctl_client* self, int timeout_ms)
while (root == NULL) { while (root == NULL) {
int n = poll(&pfd, 1, timeout_ms); int n = poll(&pfd, 1, timeout_ms);
if (n == -1) { if (n == -1) {
if (errno == EINTR)
continue;
WARN("Error waiting for a response: %m"); WARN("Error waiting for a response: %m");
break; break;
} else if (n == 0) { } else if (n == 0) {
@ -342,6 +343,35 @@ static int ctl_client_print_response(struct ctl_client* self,
return response->code; return response->code;
} }
static struct ctl_client* sig_target = NULL;
static void stop_loop(int signal)
{
sig_target->wait_for_events = false;
}
static void setup_signals(struct ctl_client* self)
{
sig_target = self;
struct sigaction sa = { 0 };
sa.sa_handler = stop_loop;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
}
static void ctl_client_event_loop(struct ctl_client* self)
{
self->wait_for_events = true;
setup_signals(self);
while (self->wait_for_events) {
DEBUG("Waiting for an event");
json_t* root = read_one_object(self, -1);
json_dumpf(root, stdout, 0);
printf("\n");
fflush(stdout);
json_decref(root);
}
}
int ctl_client_run_command(struct ctl_client* self, int ctl_client_run_command(struct ctl_client* self,
int argc, char* argv[], unsigned flags) int argc, char* argv[], unsigned flags)
{ {
@ -359,6 +389,9 @@ int ctl_client_run_command(struct ctl_client* self,
result = ctl_client_print_response(self, request, response, flags); result = ctl_client_print_response(self, request, response, flags);
if (result == 0 && strcmp(request->method, "event-receive") == 0)
ctl_client_event_loop(self);
jsonipc_response_destroy(response); jsonipc_response_destroy(response);
receive_failure: receive_failure:
send_failure: send_failure:

View File

@ -43,6 +43,7 @@ enum send_priority {
enum cmd_type { enum cmd_type {
CMD_HELP, CMD_HELP,
CMD_VERSION, CMD_VERSION,
CMD_EVENT_RECEIVE,
CMD_SET_OUTPUT, CMD_SET_OUTPUT,
CMD_UNKNOWN, CMD_UNKNOWN,
}; };
@ -70,6 +71,11 @@ static struct cmd_info cmd_list[] = {
"Query the version of the wayvnc process", "Query the version of the wayvnc process",
{{NULL, NULL}} {{NULL, NULL}}
}, },
[CMD_EVENT_RECEIVE] = { "event-receive",
"Register to begin receiving asynchronous events from wayvnc",
// TODO: Event type filtering?
{{NULL, NULL}}
},
[CMD_SET_OUTPUT] = { "set-output", [CMD_SET_OUTPUT] = { "set-output",
"Switch the actively captured output", "Switch the actively captured output",
{ {
@ -112,6 +118,7 @@ struct ctl_client {
char* write_ptr; char* write_ptr;
size_t write_len; size_t write_len;
bool drop_after_next_send; bool drop_after_next_send;
bool accept_events;
}; };
struct ctl { struct ctl {
@ -222,6 +229,7 @@ static struct cmd* parse_command(struct jsonipc_request* ipc,
cmd = (struct cmd*)cmd_set_output_new(ipc->params, err); cmd = (struct cmd*)cmd_set_output_new(ipc->params, err);
break; break;
case CMD_VERSION: case CMD_VERSION:
case CMD_EVENT_RECEIVE:
cmd = calloc(1, sizeof(*cmd)); cmd = calloc(1, sizeof(*cmd));
cmd->type = cmd_type; cmd->type = cmd_type;
break; break;
@ -344,7 +352,8 @@ static struct cmd_response* generate_version_object()
return response; return response;
} }
static struct cmd_response* ctl_server_dispatch_cmd(struct ctl* self, struct cmd* cmd) static struct cmd_response* ctl_server_dispatch_cmd(struct ctl* self,
struct ctl_client* client, struct cmd* cmd)
{ {
assert(cmd->type != CMD_UNKNOWN); assert(cmd->type != CMD_UNKNOWN);
const struct cmd_info* info = &cmd_list[cmd->type]; const struct cmd_info* info = &cmd_list[cmd->type];
@ -367,6 +376,10 @@ static struct cmd_response* ctl_server_dispatch_cmd(struct ctl* self, struct cmd
case CMD_VERSION: case CMD_VERSION:
response = generate_version_object(); response = generate_version_object();
break; break;
case CMD_EVENT_RECEIVE:
client->accept_events = true;
response = cmd_ok();
break;
case CMD_UNKNOWN: case CMD_UNKNOWN:
break; break;
} }
@ -410,6 +423,7 @@ static int client_enqueue_jsonipc(struct ctl_client* self,
goto failure; goto failure;
} }
result = client_enqueue(self, packed_response, priority); result = client_enqueue(self, packed_response, priority);
json_decref(packed_response);
if (result != 0) if (result != 0)
nvnc_log(NVNC_LOG_WARNING, "Append failed"); nvnc_log(NVNC_LOG_WARNING, "Append failed");
failure: failure:
@ -553,7 +567,7 @@ static void recv_ready(struct ctl_client* client)
// handled by the main loop instead of doing the // handled by the main loop instead of doing the
// dispatch here // dispatch here
struct cmd_response* response = struct cmd_response* response =
ctl_server_dispatch_cmd(server, cmd); ctl_server_dispatch_cmd(server, client, cmd);
if (!response) if (!response)
goto no_response; goto no_response;
client_enqueue_response(client, response, request->id); client_enqueue_response(client, response, request->id);
@ -736,3 +750,85 @@ struct cmd_response* cmd_failed(const char* fmt, ...)
va_end(ap); va_end(ap);
return resp; return resp;
} }
json_t* pack_connection_event_params(
const char* client_id,
const char* client_hostname,
const char* client_username,
int new_connection_count)
{
return json_pack("{s:s, s:s?, s:s?, s:i}",
"id", client_id,
"hostname", client_hostname,
"username", client_username,
"connection_count", new_connection_count);
}
int ctl_server_enqueue_event(struct ctl* self, const char* event_name,
json_t* params)
{
char* param_str = json_dumps(params, JSON_COMPACT);
nvnc_log(NVNC_LOG_DEBUG, "Enqueueing %s event: {%s", event_name, param_str);
free(param_str);
struct jsonipc_request* event = jsonipc_event_new(event_name, params);
json_decref(params);
json_error_t err;
json_t* packed_event = jsonipc_request_pack(event, &err);
jsonipc_request_destroy(event);
if (!packed_event) {
nvnc_log(NVNC_LOG_WARNING, "Could not pack %s event json: %s", event_name, err.text);
return -1;
}
int enqueued = 0;
struct ctl_client* client;
wl_list_for_each(client, &self->clients, link) {
if (!client->accept_events) {
nvnc_trace("Skipping event send to control client %p", client);
continue;
}
if (client_enqueue(client, packed_event, false) == 0) {
nvnc_trace("Enqueued event for control client %p", client);
enqueued++;
} else {
nvnc_trace("Failed to enqueue event for control client %p", client);
}
}
json_decref(packed_event);
nvnc_log(NVNC_LOG_DEBUG, "Enqueued %s event for %d clients", event_name, enqueued);
return enqueued;
}
void ctl_server_event_connect(struct ctl* self,
bool connected,
const char* client_id,
const char* client_hostname,
const char* client_username,
int new_connection_count)
{
json_t* params = pack_connection_event_params(client_id, client_hostname,
client_username, new_connection_count);
ctl_server_enqueue_event(self,
connected ? "client-connected" : "client-disconnected",
params);
}
void ctl_server_event_connected(struct ctl* self,
const char* client_id,
const char* client_hostname,
const char* client_username,
int new_connection_count)
{
ctl_server_event_connect(self, true, client_id, client_hostname,
client_username, new_connection_count);
}
void ctl_server_event_disconnected(struct ctl* self,
const char* client_id,
const char* client_hostname,
const char* client_username,
int new_connection_count)
{
ctl_server_event_connect(self, false, client_id, client_hostname,
client_username, new_connection_count);
}

View File

@ -844,6 +844,14 @@ static void on_client_cleanup(struct nvnc_client* client)
self->nr_clients--; self->nr_clients--;
nvnc_log(NVNC_LOG_DEBUG, "Client disconnected, new client count: %d", nvnc_log(NVNC_LOG_DEBUG, "Client disconnected, new client count: %d",
self->nr_clients); self->nr_clients);
char id[11];
snprintf(id, 11, "%p", nvnc);
ctl_server_event_disconnected(self->ctl, id,
nvnc_client_get_hostname(client),
nvnc_client_get_auth_username(client),
self->nr_clients);
if (self->nr_clients == 0) { if (self->nr_clients == 0) {
nvnc_log(NVNC_LOG_INFO, "Stopping screen capture"); nvnc_log(NVNC_LOG_INFO, "Stopping screen capture");
screencopy_stop(&self->screencopy); screencopy_stop(&self->screencopy);
@ -865,6 +873,13 @@ static void on_client_new(struct nvnc_client* client)
nvnc_set_client_cleanup_fn(client, on_client_cleanup); nvnc_set_client_cleanup_fn(client, on_client_cleanup);
nvnc_log(NVNC_LOG_DEBUG, "Client connected, new client count: %d", nvnc_log(NVNC_LOG_DEBUG, "Client connected, new client count: %d",
self->nr_clients); self->nr_clients);
char id[11];
snprintf(id, 11, "%p", nvnc);
ctl_server_event_connected(self->ctl, id,
nvnc_client_get_hostname(client),
nvnc_client_get_auth_username(client),
self->nr_clients);
} }
void parse_keyboard_option(struct wayvnc* self, char* arg) void parse_keyboard_option(struct wayvnc* self, char* arg)

View File

@ -176,6 +176,16 @@ information. Much like the _-V_ option, the response data will contain the
version numbers of wayvnc, as well as the versions of the neatvnc and aml version numbers of wayvnc, as well as the versions of the neatvnc and aml
components. components.
_EVENT-RECEIVE_
The *event-receive* command registers for asynchronous server events. See the
_EVENTS_ section below for details on the event message format, and the _IPC
EVENTS_ section below for a description of all possible server events.
Event registration registers for all available server eve ts and is scoped to
the current connection only. If a client disconnects and reconnects, it must
re-register for events.
_SET-OUTPUT_ _SET-OUTPUT_
For multi-output wayland displays, this command switches which output is For multi-output wayland displays, this command switches which output is
@ -189,6 +199,45 @@ which parameters are supplied:
*switch-to=output-name* *switch-to=output-name*
Switch to a specific output by name. Switch to a specific output by name.
## IPC EVENTS
_CLIENT-CONNECTED_
The *client-connected* event is sent when a new VNC client connects to wayvnc.
Parameters:
*id=...*
A unique identifier for this client.
*connection_count=...*
The total number of connected VNC clients including this one.
*hostname=...*
The hostname or IP of this client. May be null.
*username=...*
The username used to authenticate this client. May be null.
_CLIENT-DISCONNECTED_
The *client-disconnected* event is sent when a VNC cliwnt disconnects from
wayvnc.
Parameters:
*id=...*
A unique identifier for this client.
*connection_count=...*
The total number of connected VNC clients not including this one.
*hostname=...*
The hostname or IP of this client. May be null.
*username=...*
The username used to authenticate this client. May be null.
## IPC MESSAGE FORMAT ## IPC MESSAGE FORMAT
The *wayvncctl(1)* command line utility will construct properly-formatted json The *wayvncctl(1)* command line utility will construct properly-formatted json
@ -247,6 +296,30 @@ The *data* object contains method-specific return data. This may be structured
data in response to a query, a simple error string in the case of a failed data in response to a query, a simple error string in the case of a failed
request, or it may be omitted entirely if the error code alone is sufficient. request, or it may be omitted entirely if the error code alone is sufficient.
_EVENTS_
Events are aaynchronous messages sent from a server to all registered clients.
The message format is identical to a _REQUEST_, but without an "id" field, and a
client must not send a response.
Example event message:
```
{
"method": "event-name",
"params": {
"key1": "value1",
"key2": "value2",
}
}
```
In order to receive any events, a client must first register to receive them by
sending a _event-receive_ request IPC. Once the success response has been sent
by the server, the client must expect that asynchronous event messages may be
sent by the server at any time, even between a request and the associated
response.
# ENVIRONMENT # ENVIRONMENT
The following environment variables have an effect on wayvnc: The following environment variables have an effect on wayvnc:

View File

@ -42,6 +42,14 @@ a list of the available commands.
Running *wayvncctl command-name --help* returns a description of the server-side Running *wayvncctl command-name --help* returns a description of the server-side
command and its available parameters. command and its available parameters.
# ASYNCHRONOUS EVENTS
While *wayvncctl* normally terminates after sending one request and receiving
the corresponding reply, the *event-receive* command acts differently. Instead
of exiting immediately, *wayvncctl* waits for any events fr the server, printing
each to stdout as they arrive. This mode of operation will block until either
it receives a signal to terminate, or until the wayvnc server terminates.
# EXAMPLES # EXAMPLES
Query the server for all available IPC command names: Query the server for all available IPC command names: