| ... | ... | @@ -0,0 +1,680 @@ |
| 1 | #define _GNU_SOURCE |
| 2 | #include <errno.h> |
| 3 | #include <fcntl.h> |
| 4 | #include <signal.h> |
| 5 | #include <stdlib.h> |
| 6 | #include <string.h> |
| 7 | #include <sys/file.h> |
| 8 | #include <sys/ipc.h> |
| 9 | #include <sys/shm.h> |
| 10 | #include <unistd.h> |
| 11 | #include <gio/gunixfdlist.h> |
| 12 | #include <glib-unix.h> |
| 13 | #include <X11/Xlib.h> |
| 14 | #include <X11/Xutil.h> |
| 15 | #include <X11/extensions/Xfixes.h> |
| 16 | #include <X11/extensions/XShm.h> |
| 17 | #include <pipewire/pipewire.h> |
| 18 | #include <spa/param/video/format-utils.h> |
| 19 | #include <spa/buffer/meta.h> |
| 20 | #include "cursor.h" |
| 21 | |
| 22 | static GMainLoop *main_loop; |
| 23 | static int channel = -1, capturing, initial = 1, stopping, suspended; |
| 24 | static guint read_watch, write_watch; |
| 25 | static GByteArray *out, *cursor_packet; |
| 26 | static size_t sent; |
| 27 | static gint64 queued_at; |
| 28 | static uint8_t request[16]; |
| 29 | static size_t received, request_size = 12; |
| 30 | static uint8_t *before; |
| 31 | static uint32_t width, height, cursor_id, pointer[SG_POINTER_WORDS]; |
| 32 | static const char *channel_path; |
| 33 | static uint32_t stats[SG_CAPTURE_STATS_WORDS]; |
| 34 | static gint64 stats_at; |
| 35 | static Display *display; |
| 36 | static Window root; |
| 37 | static XImage *ximage; |
| 38 | static XShmSegmentInfo shared = {.shmid = -1}; |
| 39 | static int shm_attached, x_error, cursor_event, cursor_changed = 1; |
| 40 | static unsigned long cursor_serial; |
| 41 | static gint64 next_capture; |
| 42 | static GDBusConnection *bus, *login_bus; |
| 43 | static char *session_path, *request_path; |
| 44 | static int portal_step, session_active = 1, session_locked, restart; |
| 45 | static struct pw_main_loop *pw_loop; |
| 46 | static struct pw_context *pw_context; |
| 47 | static struct pw_core *pw_core; |
| 48 | static struct pw_stream *pw_stream; |
| 49 | static struct spa_hook stream_listener; |
| 50 | static struct spa_video_info_raw video_info; |
| 51 | static char *restore_path; |
| 52 | static guint portal_subscription, session_subscription, pw_watch; |
| 53 | |
| 54 | static void close_channel(void) { |
| 55 | if (read_watch) { g_source_remove(read_watch); read_watch = 0; } |
| 56 | if (write_watch) { g_source_remove(write_watch); write_watch = 0; } |
| 57 | if (channel >= 0) close(channel); |
| 58 | channel = -1; capturing = 0; stopping = 0; initial = 1; |
| 59 | received = 0; request_size = 12; sent = 0; |
| 60 | g_byte_array_set_size(out, 0); |
| 61 | if (pw_stream) pw_stream_set_active(pw_stream, false); |
| 62 | } |
| 63 | |
| 64 | static gboolean flush_output(gint fd, GIOCondition condition, gpointer unused) { |
| 65 | (void)fd; (void)unused; |
| 66 | write_watch = 0; |
| 67 | if (condition & (G_IO_HUP | G_IO_ERR)) { close_channel(); return G_SOURCE_REMOVE; } |
| 68 | while (sent < out->len) { |
| 69 | size_t amount = out->len - sent; |
| 70 | if (amount > 65536) amount = 65536; |
| 71 | ssize_t count = write(channel, out->data + sent, amount); |
| 72 | if (count < 0 && errno == EINTR) continue; |
| 73 | if (count < 0 && errno == EAGAIN) { |
| 74 | write_watch = g_unix_fd_add(channel, G_IO_OUT | G_IO_HUP | G_IO_ERR, flush_output, NULL); |
| 75 | return G_SOURCE_REMOVE; |
| 76 | } |
| 77 | if (count <= 0) { close_channel(); return G_SOURCE_REMOVE; } |
| 78 | sent += (size_t)count; |
| 79 | } |
| 80 | g_byte_array_set_size(out, 0); sent = 0; |
| 81 | if (stopping) close_channel(); |
| 82 | return G_SOURCE_REMOVE; |
| 83 | } |
| 84 | |
| 85 | static int packet(uint32_t type, const uint32_t *words, uint32_t count, const uint8_t *data, size_t length) { |
| 86 | if (channel < 0 || stopping) return 0; |
| 87 | size_t payload = count * 4 + length; |
| 88 | if (payload > SG_MAX_PACKET_BYTES || out->len - sent + payload + 12 > SG_MAX_PACKET_BYTES + 131072u) { |
| 89 | close_channel(); return 0; |
| 90 | } |
| 91 | uint8_t header[12 + SG_SCREEN_WORDS * 4]; |
| 92 | memcpy(header, SG_MAGIC, 4); sg_word(header + 4, type); sg_word(header + 8, (uint32_t)payload); |
| 93 | for (uint32_t i = 0; i < count; ++i) sg_word(header + 12 + i * 4, words[i]); |
| 94 | if (sent) { g_byte_array_remove_range(out, 0, (guint)sent); sent = 0; } |
| 95 | if (!out->len) queued_at = g_get_monotonic_time(); |
| 96 | g_byte_array_append(out, header, 12 + count * 4); |
| 97 | if (length) g_byte_array_append(out, data, (guint)length); |
| 98 | if (!write_watch) flush_output(channel, 0, NULL); |
| 99 | return channel >= 0; |
| 100 | } |
| 101 | |
| 102 | static void capabilities(void) { |
| 103 | const uint32_t words[SG_CAPABILITIES_WORDS] = {0, 1}; |
| 104 | packet(SG_CAPABILITIES, words, SG_CAPABILITIES_WORDS, NULL, 0); |
| 105 | } |
| 106 | |
| 107 | static void replay_cursor(void) { |
| 108 | if (cursor_packet->len) { |
| 109 | uint32_t words[SG_CURSOR_WORDS]; |
| 110 | for (uint32_t i = 0; i < SG_CURSOR_WORDS; ++i) { |
| 111 | const uint8_t *p = cursor_packet->data + i * 4; |
| 112 | words[i] = ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; |
| 113 | } |
| 114 | packet(SG_CURSOR, words, SG_CURSOR_WORDS, cursor_packet->data + sizeof(words), cursor_packet->len - sizeof(words)); |
| 115 | packet(SG_POINTER, pointer, SG_POINTER_WORDS, NULL, 0); |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | static void cursor_shape(uint32_t w, uint32_t h, int hot_x, int hot_y, const uint8_t *rgba) { |
| 120 | uint32_t words[SG_CURSOR_WORDS] = {0}; |
| 121 | words[SG_CURSOR_ID] = ++cursor_id; |
| 122 | size_t pixels = (size_t)w * h; |
| 123 | if (w && h && w <= SG_MAX_CURSOR_SIZE && h <= SG_MAX_CURSOR_SIZE && hot_x >= 0 && hot_y >= 0 && (uint32_t)hot_x < w && (uint32_t)hot_y < h) { |
| 124 | words[SG_CURSOR_WIDTH] = w; words[SG_CURSOR_HEIGHT] = h; |
| 125 | words[SG_CURSOR_HOT_X] = (uint32_t)hot_x; words[SG_CURSOR_HOT_Y] = (uint32_t)hot_y; words[SG_CURSOR_FRAMES] = 1; |
| 126 | } else pixels = 0; |
| 127 | g_byte_array_set_size(cursor_packet, sizeof(words) + (pixels ? 4 + pixels * 5 : 0)); |
| 128 | for (uint32_t i = 0; i < SG_CURSOR_WORDS; ++i) sg_word(cursor_packet->data + i * 4, words[i]); |
| 129 | if (pixels) { |
| 130 | sg_word(cursor_packet->data + sizeof(words), 1000); |
| 131 | memcpy(cursor_packet->data + sizeof(words) + 4, rgba, pixels * 4); |
| 132 | memset(cursor_packet->data + sizeof(words) + 4 + pixels * 4, 0, pixels); |
| 133 | } |
| 134 | pointer[SG_POINTER_ID] = cursor_id; |
| 135 | replay_cursor(); |
| 136 | } |
| 137 | |
| 138 | static void position(int x, int y, int visible) { |
| 139 | uint32_t words[SG_POINTER_WORDS] = {cursor_id, (uint32_t)x, (uint32_t)y, (uint32_t)visible}; |
| 140 | if (memcmp(pointer, words, sizeof(words))) { |
| 141 | memcpy(pointer, words, sizeof(words)); |
| 142 | packet(SG_POINTER, words, SG_POINTER_WORDS, NULL, 0); |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | static void screen_frame(const uint8_t *pixels, uint32_t w, uint32_t h) { |
| 147 | if (!w || !h || w > SG_MAX_DIMENSION || h > SG_MAX_DIMENSION || w * h > SG_MAX_PIXELS) return; |
| 148 | if (out->len - sent > 65536 || channel < 0 || !capturing || suspended) return; |
| 149 | gint64 start = g_get_monotonic_time(); |
| 150 | if (w != width || h != height) { |
| 151 | free(before); before = malloc((size_t)w * h * 4); |
| 152 | if (!before) { close_channel(); return; } |
| 153 | width = w; height = h; initial = 1; |
| 154 | } |
| 155 | struct sg_rect rect = {0, 0, w, h}; |
| 156 | if (!initial && !sg_difference(before, pixels, w, h, &rect)) return; |
| 157 | stats[SG_CAPTURE_STATS_SCAN_US] += (uint32_t)(g_get_monotonic_time() - start); |
| 158 | start = g_get_monotonic_time(); |
| 159 | uint32_t words[SG_SCREEN_WORDS] = {w, h, rect.x, rect.y, rect.width, rect.height}; |
| 160 | size_t length = (size_t)rect.width * rect.height * 4; |
| 161 | uint8_t *packed = malloc(length); |
| 162 | if (!packed) { close_channel(); return; } |
| 163 | for (uint32_t y = 0; y < rect.height; ++y) { |
| 164 | size_t offset = ((size_t)(rect.y + y) * w + rect.x) * 4; |
| 165 | memcpy(packed + (size_t)y * rect.width * 4, pixels + offset, rect.width * 4); |
| 166 | memcpy(before + offset, pixels + offset, rect.width * 4); |
| 167 | } |
| 168 | if (initial) replay_cursor(); |
| 169 | if (packet(SG_SCREEN, words, SG_SCREEN_WORDS, packed, length)) initial = 0; |
| 170 | free(packed); |
| 171 | stats[SG_CAPTURE_STATS_WRITE_US] += (uint32_t)(g_get_monotonic_time() - start); |
| 172 | } |
| 173 | |
| 174 | static gboolean read_requests(gint fd, GIOCondition condition, gpointer unused) { |
| 175 | (void)unused; |
| 176 | if (condition & (G_IO_HUP | G_IO_ERR)) { read_watch = 0; close_channel(); return G_SOURCE_REMOVE; } |
| 177 | for (;;) { |
| 178 | ssize_t count = read(fd, request + received, request_size - received); |
| 179 | if (count < 0 && errno == EINTR) continue; |
| 180 | if (count < 0 && errno == EAGAIN) return G_SOURCE_CONTINUE; |
| 181 | if (count <= 0) break; |
| 182 | received += (size_t)count; |
| 183 | if (received < request_size) continue; |
| 184 | if (request_size == 12) { |
| 185 | if (memcmp(request, SG_MAGIC, 4) || request[4] || request[5] || request[6] || request[8] || request[9] || request[10]) break; |
| 186 | if ((request[7] == SG_STREAM || request[7] == SG_EXPERIMENT) && request[11] == 4) { request_size = 16; continue; } |
| 187 | if (request[11]) break; |
| 188 | if (request[7] == SG_STOP) { |
| 189 | read_watch = 0; |
| 190 | capturing = 0; |
| 191 | if (pw_stream) pw_stream_set_active(pw_stream, false); |
| 192 | packet(SG_STOP, NULL, 0, NULL, 0); |
| 193 | stopping = 1; |
| 194 | if (!out->len) close_channel(); |
| 195 | return G_SOURCE_REMOVE; |
| 196 | } |
| 197 | if (request[7] != SG_REFRESH) break; |
| 198 | initial = 1; cursor_changed = 1; capabilities(); replay_cursor(); |
| 199 | } else { |
| 200 | if (request[12] || request[13] || request[14] || request[15] > 1) break; |
| 201 | if (request[7] == SG_STREAM) { |
| 202 | int enabled = request[15]; |
| 203 | if (capturing != enabled) { |
| 204 | capturing = enabled; initial = 1; cursor_changed = 1; |
| 205 | if (enabled) replay_cursor(); |
| 206 | if (pw_stream) pw_stream_set_active(pw_stream, capturing && !suspended); |
| 207 | } |
| 208 | } |
| 209 | } |
| 210 | received = 0; request_size = 12; |
| 211 | if (channel < 0) { read_watch = 0; return G_SOURCE_REMOVE; } |
| 212 | } |
| 213 | read_watch = 0; close_channel(); return G_SOURCE_REMOVE; |
| 214 | } |
| 215 | |
| 216 | static int x_error_handler(Display *d, XErrorEvent *e) { (void)d; (void)e; x_error = 1; return 0; } |
| 217 | |
| 218 | static void free_ximage(void) { |
| 219 | if (shm_attached) { XShmDetach(display, &shared); XSync(display, False); shm_attached = 0; } |
| 220 | if (ximage) { |
| 221 | if (shared.shmid >= 0) ximage->data = NULL; |
| 222 | XDestroyImage(ximage); ximage = NULL; |
| 223 | } |
| 224 | if (shared.shmid >= 0) { shmdt(shared.shmaddr); shmctl(shared.shmid, IPC_RMID, NULL); shared.shmid = -1; } |
| 225 | } |
| 226 | |
| 227 | static void x_cursor(void) { |
| 228 | XFixesCursorImage *image = XFixesGetCursorImage(display); |
| 229 | if (!image) return; |
| 230 | if (cursor_changed || image->cursor_serial != cursor_serial) { |
| 231 | uint8_t rgba[SG_MAX_CURSOR_SIZE * SG_MAX_CURSOR_SIZE * 4]; |
| 232 | uint32_t w = image->width, h = image->height; |
| 233 | int visible = 0; |
| 234 | if (w <= SG_MAX_CURSOR_SIZE && h <= SG_MAX_CURSOR_SIZE) { |
| 235 | for (size_t i = 0; i < (size_t)w * h; ++i) { |
| 236 | uint32_t pixel = (uint32_t)image->pixels[i]; |
| 237 | unsigned alpha = pixel >> 24; |
| 238 | visible |= alpha != 0; |
| 239 | rgba[i * 4 + 3] = (uint8_t)alpha; |
| 240 | for (unsigned c = 0; c < 3; ++c) { |
| 241 | unsigned value = (pixel >> (16 - c * 8)) & 255; |
| 242 | value = alpha ? (value * 255 + alpha / 2) / alpha : 0; |
| 243 | rgba[i * 4 + c] = (uint8_t)(value > 255 ? 255 : value); |
| 244 | } |
| 245 | } |
| 246 | position(image->x, image->y, visible); |
| 247 | cursor_shape(w, h, image->xhot, image->yhot, rgba); |
| 248 | } else { position(image->x, image->y, 1); cursor_shape(0, 0, 0, 0, NULL); } |
| 249 | cursor_serial = image->cursor_serial; cursor_changed = 0; |
| 250 | } else position(image->x, image->y, pointer[SG_POINTER_VISIBLE]); |
| 251 | XFree(image); |
| 252 | } |
| 253 | |
| 254 | static gboolean x_tick(gpointer unused) { |
| 255 | (void)unused; |
| 256 | while (XPending(display)) { |
| 257 | XEvent event; XNextEvent(display, &event); |
| 258 | if (event.type == cursor_event + XFixesCursorNotify) cursor_changed = 1; |
| 259 | } |
| 260 | if (!capturing || suspended || channel < 0 || stopping) return G_SOURCE_CONTINUE; |
| 261 | x_cursor(); |
| 262 | gint64 now = g_get_monotonic_time(); |
| 263 | if (now < next_capture || out->len - sent > 65536) return G_SOURCE_CONTINUE; |
| 264 | next_capture = now + 16667; |
| 265 | XWindowAttributes attr; |
| 266 | x_error = 0; |
| 267 | if (!XGetWindowAttributes(display, root, &attr) || attr.width <= 0 || attr.height <= 0 || attr.width > (int)SG_MAX_DIMENSION || attr.height > (int)SG_MAX_DIMENSION || (uint32_t)attr.width * attr.height > SG_MAX_PIXELS) return G_SOURCE_CONTINUE; |
| 268 | if (ximage && (ximage->width != attr.width || ximage->height != attr.height)) free_ximage(); |
| 269 | if (!ximage && XShmQueryExtension(display)) { |
| 270 | ximage = XShmCreateImage(display, attr.visual, (unsigned)attr.depth, ZPixmap, NULL, &shared, (unsigned)attr.width, (unsigned)attr.height); |
| 271 | if (ximage) { |
| 272 | shared.shmid = shmget(IPC_PRIVATE, (size_t)ximage->bytes_per_line * ximage->height, IPC_CREAT | 0600); |
| 273 | if (shared.shmid >= 0) { |
| 274 | shared.shmaddr = shmat(shared.shmid, NULL, 0); shared.readOnly = False; |
| 275 | if (shared.shmaddr != (void *)-1) { |
| 276 | ximage->data = shared.shmaddr; |
| 277 | shm_attached = XShmAttach(display, &shared); XSync(display, False); |
| 278 | if (x_error) shm_attached = 0; |
| 279 | shmctl(shared.shmid, IPC_RMID, NULL); |
| 280 | } |
| 281 | } |
| 282 | if (!shm_attached) free_ximage(); |
| 283 | } |
| 284 | } |
| 285 | int temporary = !ximage; |
| 286 | XImage *image = ximage; |
| 287 | if (image) { if (!XShmGetImage(display, root, image, 0, 0, AllPlanes)) return G_SOURCE_CONTINUE; } |
| 288 | else image = XGetImage(display, root, 0, 0, (unsigned)attr.width, (unsigned)attr.height, AllPlanes, ZPixmap); |
| 289 | if (!image || x_error) { if (temporary && image) XDestroyImage(image); return G_SOURCE_CONTINUE; } |
| 290 | ++stats[SG_CAPTURE_STATS_FRAMES]; |
| 291 | stats[SG_CAPTURE_STATS_CAPTURE_US] += (uint32_t)(g_get_monotonic_time() - now); |
| 292 | if (image->bits_per_pixel == 32 && image->byte_order == LSBFirst && image->red_mask == 0xff0000 && image->green_mask == 0xff00 && image->blue_mask == 0xff && image->bytes_per_line == image->width * 4) { |
| 293 | screen_frame((uint8_t *)image->data, (uint32_t)image->width, (uint32_t)image->height); |
| 294 | } else { |
| 295 | uint8_t *pixels = malloc((size_t)image->width * image->height * 4); |
| 296 | if (pixels) { |
| 297 | unsigned shifts[3] = {0}, ranges[3]; |
| 298 | unsigned long masks[3] = {image->blue_mask, image->green_mask, image->red_mask}; |
| 299 | for (unsigned c = 0; c < 3; ++c) { |
| 300 | while (masks[c] && !(masks[c] & 1)) { ++shifts[c]; masks[c] >>= 1; } |
| 301 | ranges[c] = (unsigned)masks[c]; |
| 302 | } |
| 303 | for (int y = 0; y < image->height; ++y) for (int x = 0; x < image->width; ++x) { |
| 304 | unsigned long pixel = XGetPixel(image, x, y); |
| 305 | size_t offset = ((size_t)y * image->width + x) * 4; |
| 306 | for (unsigned c = 0; c < 3; ++c) pixels[offset + c] = ranges[c] ? (uint8_t)(((pixel >> shifts[c]) & ranges[c]) * 255 / ranges[c]) : 0; |
| 307 | pixels[offset + 3] = 0; |
| 308 | } |
| 309 | screen_frame(pixels, (uint32_t)image->width, (uint32_t)image->height); free(pixels); |
| 310 | } |
| 311 | } |
| 312 | if (temporary) XDestroyImage(image); |
| 313 | return G_SOURCE_CONTINUE; |
| 314 | } |
| 315 | |
| 316 | static void pw_cursor(struct spa_buffer *buffer) { |
| 317 | struct spa_meta *meta = spa_buffer_find_meta(buffer, SPA_META_Cursor); |
| 318 | if (!meta || meta->size < sizeof(struct spa_meta_cursor)) return; |
| 319 | struct spa_meta_cursor cursor; memcpy(&cursor, meta->data, sizeof(cursor)); |
| 320 | if (!cursor.id) return; |
| 321 | position(cursor.position.x, cursor.position.y, pointer[SG_POINTER_VISIBLE]); |
| 322 | if (!cursor.bitmap_offset) return; |
| 323 | if (cursor.bitmap_offset < sizeof(cursor) || cursor.bitmap_offset > meta->size - sizeof(struct spa_meta_bitmap)) return; |
| 324 | struct spa_meta_bitmap bitmap; memcpy(&bitmap, (uint8_t *)meta->data + cursor.bitmap_offset, sizeof(bitmap)); |
| 325 | if (!bitmap.format) return; |
| 326 | if (!bitmap.offset) { position(cursor.position.x, cursor.position.y, 0); return; } |
| 327 | uint32_t w = bitmap.size.width, h = bitmap.size.height; |
| 328 | size_t available = meta->size - cursor.bitmap_offset; |
| 329 | if (!w || !h || w > SG_MAX_CURSOR_SIZE || h > SG_MAX_CURSOR_SIZE || bitmap.stride < (int32_t)w * 4 || bitmap.offset < sizeof(bitmap) || bitmap.offset > available || (size_t)(h - 1) * bitmap.stride + w * 4 > available - bitmap.offset) { |
| 330 | position(cursor.position.x, cursor.position.y, 1); cursor_shape(0, 0, 0, 0, NULL); return; |
| 331 | } |
| 332 | unsigned offsets[4]; |
| 333 | switch (bitmap.format) { |
| 334 | case SPA_VIDEO_FORMAT_RGBA: offsets[0] = 0; offsets[1] = 1; offsets[2] = 2; offsets[3] = 3; break; |
| 335 | case SPA_VIDEO_FORMAT_BGRA: offsets[0] = 2; offsets[1] = 1; offsets[2] = 0; offsets[3] = 3; break; |
| 336 | case SPA_VIDEO_FORMAT_ARGB: offsets[0] = 1; offsets[1] = 2; offsets[2] = 3; offsets[3] = 0; break; |
| 337 | case SPA_VIDEO_FORMAT_ABGR: offsets[0] = 3; offsets[1] = 2; offsets[2] = 1; offsets[3] = 0; break; |
| 338 | default: position(cursor.position.x, cursor.position.y, 1); cursor_shape(0, 0, 0, 0, NULL); return; |
| 339 | } |
| 340 | uint8_t rgba[SG_MAX_CURSOR_SIZE * SG_MAX_CURSOR_SIZE * 4]; |
| 341 | const uint8_t *pixels = (uint8_t *)meta->data + cursor.bitmap_offset + bitmap.offset; |
| 342 | int visible = 0; |
| 343 | for (uint32_t y = 0; y < h; ++y) for (uint32_t x = 0; x < w; ++x) { |
| 344 | const uint8_t *src = pixels + (size_t)y * bitmap.stride + x * 4; |
| 345 | uint8_t *dst = rgba + ((size_t)y * w + x) * 4; |
| 346 | unsigned alpha = src[offsets[3]]; dst[3] = (uint8_t)alpha; visible |= alpha != 0; |
| 347 | for (unsigned c = 0; c < 3; ++c) { |
| 348 | unsigned value = alpha ? (src[offsets[c]] * 255u + alpha / 2) / alpha : 0; |
| 349 | dst[c] = (uint8_t)(value > 255 ? 255 : value); |
| 350 | } |
| 351 | } |
| 352 | position(cursor.position.x, cursor.position.y, visible); |
| 353 | cursor_shape(w, h, cursor.hotspot.x, cursor.hotspot.y, rgba); |
| 354 | } |
| 355 | |
| 356 | static void pw_process(void *unused) { |
| 357 | (void)unused; |
| 358 | struct pw_buffer *current = NULL, *next; |
| 359 | while ((next = pw_stream_dequeue_buffer(pw_stream))) { |
| 360 | /* Cursor updates can exist on a buffer whose video is superseded. */ |
| 361 | if (capturing && !suspended) pw_cursor(next->buffer); |
| 362 | if (current) pw_stream_queue_buffer(pw_stream, current); |
| 363 | current = next; |
| 364 | } |
| 365 | if (!current) return; |
| 366 | struct spa_buffer *buffer = current->buffer; |
| 367 | uint32_t w = video_info.size.width, h = video_info.size.height; |
| 368 | if (capturing && !suspended && buffer->n_datas && w && h && w <= SG_MAX_DIMENSION && h <= SG_MAX_DIMENSION && w * h <= SG_MAX_PIXELS) { |
| 369 | struct spa_data *data = &buffer->datas[0]; |
| 370 | struct spa_meta_videotransform *transform = spa_buffer_find_meta_data(buffer, SPA_META_VideoTransform, sizeof(*transform)); |
| 371 | if (transform && transform->transform != SPA_META_TRANSFORMATION_None) { |
| 372 | packet(SG_SUSPEND, NULL, 0, NULL, 0); initial = 1; |
| 373 | } else if (data->data && data->chunk && !(data->chunk->flags & SPA_CHUNK_FLAG_CORRUPTED)) { |
| 374 | int32_t stride = data->chunk->stride; |
| 375 | size_t offset = data->chunk->offset; |
| 376 | size_t length = stride > 0 ? (size_t)(h - 1) * stride + w * 4 : 0; |
| 377 | if (stride >= (int32_t)w * 4 && offset <= data->maxsize && length <= data->maxsize - offset && length <= data->chunk->size) { |
| 378 | gint64 start = g_get_monotonic_time(); |
| 379 | uint8_t *pixels = malloc((size_t)w * h * 4); |
| 380 | if (pixels) { |
| 381 | for (uint32_t y = 0; y < h; ++y) memcpy(pixels + (size_t)y * w * 4, (uint8_t *)data->data + offset + (size_t)y * stride, w * 4); |
| 382 | ++stats[SG_CAPTURE_STATS_FRAMES]; |
| 383 | stats[SG_CAPTURE_STATS_CAPTURE_US] += (uint32_t)(g_get_monotonic_time() - start); |
| 384 | screen_frame(pixels, w, h); free(pixels); |
| 385 | } |
| 386 | } |
| 387 | } |
| 388 | } |
| 389 | pw_stream_queue_buffer(pw_stream, current); |
| 390 | } |
| 391 | |
| 392 | static void pw_format(void *unused, uint32_t id, const struct spa_pod *param) { |
| 393 | (void)unused; |
| 394 | if (id != SPA_PARAM_Format || !param) return; |
| 395 | memset(&video_info, 0, sizeof(video_info)); |
| 396 | if (spa_format_video_raw_parse(param, &video_info) < 0 || video_info.format != SPA_VIDEO_FORMAT_BGRx) { close_channel(); return; } |
| 397 | uint8_t memory[1024]; struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(memory, sizeof(memory)); |
| 398 | const struct spa_pod *params[3]; |
| 399 | params[0] = spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, |
| 400 | SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(3, 2, 8), |
| 401 | SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1 << SPA_DATA_MemPtr) | (1 << SPA_DATA_MemFd))); |
| 402 | params[1] = spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, |
| 403 | SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor), |
| 404 | SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_cursor) + sizeof(struct spa_meta_bitmap) + SG_MAX_CURSOR_SIZE * SG_MAX_CURSOR_SIZE * 4)); |
| 405 | params[2] = spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, |
| 406 | SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoTransform), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_videotransform))); |
| 407 | pw_stream_update_params(pw_stream, params, 3); |
| 408 | initial = 1; |
| 409 | } |
| 410 | |
| 411 | static void capture_failed(void) { |
| 412 | if (!g_main_loop_is_running(main_loop)) return; |
| 413 | packet(SG_SUSPEND, NULL, 0, NULL, 0); |
| 414 | restart = 1; g_main_loop_quit(main_loop); |
| 415 | } |
| 416 | |
| 417 | static void pw_state(void *unused, enum pw_stream_state old, enum pw_stream_state state, const char *error) { |
| 418 | (void)unused; (void)old; |
| 419 | if (state == PW_STREAM_STATE_ERROR || state == PW_STREAM_STATE_UNCONNECTED) { |
| 420 | if (error) g_printerr("PipeWire: %s\n", error); |
| 421 | capture_failed(); |
| 422 | } |
| 423 | } |
| 424 | |
| 425 | static const struct pw_stream_events stream_events = { |
| 426 | PW_VERSION_STREAM_EVENTS, .state_changed = pw_state, .param_changed = pw_format, .process = pw_process, |
| 427 | }; |
| 428 | |
| 429 | static gboolean pw_dispatch(gint fd, GIOCondition condition, gpointer unused) { |
| 430 | (void)fd; (void)unused; |
| 431 | if ((condition & (G_IO_HUP | G_IO_ERR)) || pw_loop_iterate(pw_main_loop_get_loop(pw_loop), 0) < 0) { |
| 432 | capture_failed(); pw_watch = 0; return G_SOURCE_REMOVE; |
| 433 | } |
| 434 | return G_SOURCE_CONTINUE; |
| 435 | } |
| 436 | |
| 437 | static void portal_call(const char *method, GVariantBuilder *options); |
| 438 | |
| 439 | static void portal_closed(GDBusConnection *connection, const char *sender, const char *path, const char *interface, const char *signal, GVariant *parameters, gpointer unused) { |
| 440 | (void)connection; (void)sender; (void)path; (void)interface; (void)signal; (void)parameters; (void)unused; |
| 441 | if (pw_stream) pw_stream_set_active(pw_stream, false); |
| 442 | capture_failed(); |
| 443 | g_clear_pointer(&session_path, g_free); |
| 444 | } |
| 445 | |
| 446 | static void portal_response(GDBusConnection *connection, const char *sender, const char *path, const char *interface, const char *signal, GVariant *parameters, gpointer unused) { |
| 447 | (void)connection; (void)sender; (void)interface; (void)signal; (void)unused; |
| 448 | if (!request_path || strcmp(path, request_path)) return; |
| 449 | guint response; GVariant *results; |
| 450 | g_variant_get(parameters, "(u@a{sv})", &response, &results); |
| 451 | g_clear_pointer(&request_path, g_free); |
| 452 | if (response) { g_printerr("ScreenCast request ended: %u\n", response); portal_step = -1; if (suspended) capture_failed(); g_variant_unref(results); return; } |
| 453 | GVariantBuilder options; g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT); |
| 454 | if (portal_step == 0) { |
| 455 | const char *session; |
| 456 | if (!g_variant_lookup(results, "session_handle", "&s", &session)) { capture_failed(); goto done; } |
| 457 | session_path = g_strdup(session); |
| 458 | session_subscription = g_dbus_connection_signal_subscribe(bus, "org.freedesktop.portal.Desktop", "org.freedesktop.portal.Session", "Closed", session_path, NULL, G_DBUS_SIGNAL_FLAGS_NONE, portal_closed, NULL, NULL); |
| 459 | g_variant_builder_add(&options, "{sv}", "types", g_variant_new_uint32(1)); |
| 460 | g_variant_builder_add(&options, "{sv}", "multiple", g_variant_new_boolean(false)); |
| 461 | g_variant_builder_add(&options, "{sv}", "cursor_mode", g_variant_new_uint32(4)); |
| 462 | g_variant_builder_add(&options, "{sv}", "persist_mode", g_variant_new_uint32(2)); |
| 463 | char *token = NULL; |
| 464 | if (g_file_get_contents(restore_path, &token, NULL, NULL) && *token) g_variant_builder_add(&options, "{sv}", "restore_token", g_variant_new_string(token)); |
| 465 | g_free(token); ++portal_step; |
| 466 | portal_call("SelectSources", &options); |
| 467 | } else if (portal_step == 1) { |
| 468 | ++portal_step; |
| 469 | portal_call("Start", &options); |
| 470 | } else { |
| 471 | GVariant *streams = g_variant_lookup_value(results, "streams", G_VARIANT_TYPE("a(ua{sv})")); |
| 472 | if (!streams || g_variant_n_children(streams) != 1) { if (streams) g_variant_unref(streams); capture_failed(); goto done; } |
| 473 | guint node; GVariant *properties; |
| 474 | g_variant_get_child(streams, 0, "(u@a{sv})", &node, &properties); |
| 475 | guint64 serial = 0; g_variant_lookup(properties, "pipewire-serial", "t", &serial); |
| 476 | char target[32]; g_snprintf(target, sizeof(target), "%" G_GUINT64_FORMAT, serial); |
| 477 | const char *token; |
| 478 | if (g_variant_lookup(results, "restore_token", "&s", &token)) { |
| 479 | char *directory = g_path_get_dirname(restore_path); |
| 480 | if (!g_mkdir_with_parents(directory, 0700)) { |
| 481 | int fd = open(restore_path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0600); |
| 482 | if (fd >= 0) { if (write(fd, token, strlen(token)) < 0) g_printerr("Could not save ScreenCast token\n"); close(fd); } |
| 483 | } |
| 484 | g_free(directory); |
| 485 | } |
| 486 | GUnixFDList *fds = NULL; GError *error = NULL; |
| 487 | GVariant *reply = g_dbus_connection_call_with_unix_fd_list_sync(bus, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.ScreenCast", "OpenPipeWireRemote", |
| 488 | g_variant_new("(o@a{sv})", session_path, g_variant_new_array(G_VARIANT_TYPE("{sv}"), NULL, 0)), G_VARIANT_TYPE("(h)"), G_DBUS_CALL_FLAGS_NONE, 5000, NULL, &fds, NULL, &error); |
| 489 | if (reply) { |
| 490 | int index; g_variant_get(reply, "(h)", &index); |
| 491 | int fd = g_unix_fd_list_get(fds, index, &error); |
| 492 | if (fd >= 0) { |
| 493 | pw_loop = pw_main_loop_new(NULL); |
| 494 | if (pw_loop) pw_loop_enter(pw_main_loop_get_loop(pw_loop)); |
| 495 | pw_context = pw_loop ? pw_context_new(pw_main_loop_get_loop(pw_loop), NULL, 0) : NULL; |
| 496 | if (pw_context) pw_core = pw_context_connect_fd(pw_context, fd, NULL, 0); |
| 497 | else close(fd); |
| 498 | if (pw_core) { |
| 499 | struct pw_properties *settings = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", NULL); |
| 500 | if (serial) pw_properties_set(settings, PW_KEY_TARGET_OBJECT, target); |
| 501 | pw_stream = pw_stream_new(pw_core, "Snowglobe guest", settings); |
| 502 | } |
| 503 | if (pw_stream) { |
| 504 | pw_stream_add_listener(pw_stream, &stream_listener, &stream_events, NULL); |
| 505 | uint8_t memory[512]; struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(memory, sizeof(memory)); |
| 506 | const struct spa_pod *format = spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, |
| 507 | SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), |
| 508 | SPA_FORMAT_VIDEO_format, SPA_POD_Id(SPA_VIDEO_FORMAT_BGRx), |
| 509 | SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(&SPA_RECTANGLE(1280, 720), &SPA_RECTANGLE(1, 1), &SPA_RECTANGLE(SG_MAX_DIMENSION, SG_MAX_DIMENSION)), |
| 510 | SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(&SPA_FRACTION(60, 1), &SPA_FRACTION(0, 1), &SPA_FRACTION(60, 1))); |
| 511 | if (pw_stream_connect(pw_stream, PW_DIRECTION_INPUT, serial ? PW_ID_ANY : node, PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_INACTIVE, &format, 1) >= 0) { |
| 512 | pw_watch = g_unix_fd_add(pw_loop_get_fd(pw_main_loop_get_loop(pw_loop)), G_IO_IN | G_IO_ERR | G_IO_HUP, pw_dispatch, NULL); |
| 513 | pw_stream_set_active(pw_stream, capturing && !suspended); |
| 514 | } |
| 515 | } |
| 516 | } |
| 517 | g_variant_unref(reply); g_object_unref(fds); |
| 518 | } |
| 519 | if (error) { g_printerr("ScreenCast remote: %s\n", error->message); g_error_free(error); } |
| 520 | g_variant_unref(properties); g_variant_unref(streams); |
| 521 | if (!pw_watch) capture_failed(); |
| 522 | } |
| 523 | done: |
| 524 | g_variant_unref(results); |
| 525 | } |
| 526 | |
| 527 | static void portal_called(GObject *object, GAsyncResult *result, gpointer unused) { |
| 528 | (void)unused; |
| 529 | GError *error = NULL; |
| 530 | GVariant *reply = g_dbus_connection_call_finish(G_DBUS_CONNECTION(object), result, &error); |
| 531 | if (reply) g_variant_unref(reply); |
| 532 | if (error) { g_printerr("ScreenCast: %s\n", error->message); g_error_free(error); g_clear_pointer(&request_path, g_free); capture_failed(); } |
| 533 | } |
| 534 | |
| 535 | static void portal_call(const char *method, GVariantBuilder *options) { |
| 536 | static unsigned sequence; |
| 537 | char token[64]; g_snprintf(token, sizeof(token), "snowglobe_%u_%u", (unsigned)getpid(), ++sequence); |
| 538 | const char *sender = g_dbus_connection_get_unique_name(bus); |
| 539 | char *name = g_strdup(sender + 1); |
| 540 | for (char *p = name; *p; ++p) if (*p == '.') *p = '_'; |
| 541 | request_path = g_strdup_printf("/org/freedesktop/portal/desktop/request/%s/%s", name, token); g_free(name); |
| 542 | g_variant_builder_add(options, "{sv}", "handle_token", g_variant_new_string(token)); |
| 543 | GVariant *args; |
| 544 | if (!portal_step) args = g_variant_new("(@a{sv})", g_variant_builder_end(options)); |
| 545 | else if (portal_step == 1) args = g_variant_new("(o@a{sv})", session_path, g_variant_builder_end(options)); |
| 546 | else args = g_variant_new("(os@a{sv})", session_path, "", g_variant_builder_end(options)); |
| 547 | g_dbus_connection_call(bus, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.ScreenCast", method, args, G_VARIANT_TYPE("(o)"), G_DBUS_CALL_FLAGS_NONE, 5000, NULL, portal_called, NULL); |
| 548 | } |
| 549 | |
| 550 | static void login_state(GVariant *properties) { |
| 551 | gboolean value; |
| 552 | if (g_variant_lookup(properties, "Active", "b", &value)) session_active = value; |
| 553 | if (g_variant_lookup(properties, "LockedHint", "b", &value)) session_locked = value; |
| 554 | int blocked = !session_active || session_locked; |
| 555 | if (blocked == suspended) return; |
| 556 | suspended = blocked; initial = 1; cursor_changed = 1; |
| 557 | if (blocked) { packet(SG_SUSPEND, NULL, 0, NULL, 0); close_channel(); } |
| 558 | else { capabilities(); replay_cursor(); } |
| 559 | if (pw_stream) pw_stream_set_active(pw_stream, capturing && !suspended); |
| 560 | } |
| 561 | |
| 562 | static void login_changed(GDBusConnection *connection, const char *sender, const char *path, const char *interface, const char *signal, GVariant *parameters, gpointer unused) { |
| 563 | (void)connection; (void)sender; (void)path; (void)interface; (void)signal; (void)unused; |
| 564 | const char *name; GVariant *properties, *invalid; |
| 565 | g_variant_get(parameters, "(&s@a{sv}@as)", &name, &properties, &invalid); |
| 566 | if (!strcmp(name, "org.freedesktop.login1.Session")) login_state(properties); |
| 567 | g_variant_unref(properties); g_variant_unref(invalid); |
| 568 | } |
| 569 | |
| 570 | static gboolean heartbeat(gpointer unused) { |
| 571 | (void)unused; |
| 572 | if (suspended) { stats_at = g_get_monotonic_time(); memset(stats, 0, sizeof(stats)); return G_SOURCE_CONTINUE; } |
| 573 | if (bus && !portal_step && !request_path && !session_path) { |
| 574 | GVariantBuilder options; g_variant_builder_init(&options, G_VARIANT_TYPE_VARDICT); |
| 575 | char token[64]; g_snprintf(token, sizeof(token), "snowglobe_session_%u", (unsigned)getpid()); |
| 576 | g_variant_builder_add(&options, "{sv}", "session_handle_token", g_variant_new_string(token)); |
| 577 | portal_call("CreateSession", &options); |
| 578 | } |
| 579 | if (out->len && g_get_monotonic_time() - queued_at >= 3000000) close_channel(); |
| 580 | if (channel < 0) { |
| 581 | channel = open(channel_path, O_RDWR | O_NONBLOCK | O_CLOEXEC); |
| 582 | if (channel >= 0) { |
| 583 | if (flock(channel, LOCK_EX | LOCK_NB)) { close(channel); channel = -1; return G_SOURCE_CONTINUE; } |
| 584 | read_watch = g_unix_fd_add(channel, G_IO_IN | G_IO_ERR | G_IO_HUP, read_requests, NULL); |
| 585 | capabilities(); |
| 586 | } |
| 587 | } |
| 588 | if (channel >= 0 && !stopping) { |
| 589 | gint64 now = g_get_monotonic_time(); |
| 590 | stats[SG_CAPTURE_STATS_ELAPSED_MS] = (uint32_t)((now - stats_at) / 1000); |
| 591 | packet(SG_CAPTURE_STATS, stats, SG_CAPTURE_STATS_WORDS, NULL, 0); |
| 592 | memset(stats, 0, sizeof(stats)); stats_at = now; |
| 593 | packet(SG_HEARTBEAT, NULL, 0, NULL, 0); |
| 594 | } else stats_at = g_get_monotonic_time(); |
| 595 | return G_SOURCE_CONTINUE; |
| 596 | } |
| 597 | |
| 598 | static gboolean quit(gpointer unused) { (void)unused; g_main_loop_quit(main_loop); return G_SOURCE_REMOVE; } |
| 599 | |
| 600 | int main(int argc, char **argv) { |
| 601 | if (argc > 2) return 2; |
| 602 | channel_path = argc == 2 ? argv[1] : "/dev/virtio-ports/" SG_CHANNEL_NAME; |
| 603 | signal(SIGPIPE, SIG_IGN); |
| 604 | pw_init(NULL, NULL); |
| 605 | main_loop = g_main_loop_new(NULL, false); |
| 606 | out = g_byte_array_new(); cursor_packet = g_byte_array_new(); stats_at = g_get_monotonic_time(); |
| 607 | pointer[SG_POINTER_VISIBLE] = 1; |
| 608 | if (getenv("WAYLAND_DISPLAY") || (getenv("XDG_SESSION_TYPE") && !strcmp(getenv("XDG_SESSION_TYPE"), "wayland"))) { |
| 609 | bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL); |
| 610 | if (!bus) { restart = 1; goto cleanup; } |
| 611 | restore_path = g_build_filename(g_get_user_config_dir(), "snowglobe", "screencast-token", NULL); |
| 612 | GVariant *reply = g_dbus_connection_call_sync(bus, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.DBus.Properties", "Get", |
| 613 | g_variant_new("(ss)", "org.freedesktop.portal.ScreenCast", "AvailableCursorModes"), G_VARIANT_TYPE("(v)"), G_DBUS_CALL_FLAGS_NONE, 5000, NULL, NULL); |
| 614 | if (!reply) { restart = 1; goto cleanup; } |
| 615 | guint modes = 0; |
| 616 | GVariant *value; g_variant_get(reply, "(v)", &value); if (g_variant_is_of_type(value, G_VARIANT_TYPE_UINT32)) modes = g_variant_get_uint32(value); g_variant_unref(value); g_variant_unref(reply); |
| 617 | if (!(modes & 4)) { g_printerr("ScreenCast cursor metadata is unavailable\n"); return 1; } |
| 618 | portal_subscription = g_dbus_connection_signal_subscribe(bus, "org.freedesktop.portal.Desktop", "org.freedesktop.portal.Request", "Response", NULL, NULL, G_DBUS_SIGNAL_FLAGS_NONE, portal_response, NULL, NULL); |
| 619 | } else { |
| 620 | display = XOpenDisplay(NULL); |
| 621 | if (!display || !XFixesQueryExtension(display, &cursor_event, &(int){0})) return 1; |
| 622 | int major = 2, minor = 0; |
| 623 | if (!XFixesQueryVersion(display, &major, &minor) || major < 2) return 1; |
| 624 | XSetErrorHandler(x_error_handler); root = DefaultRootWindow(display); |
| 625 | XFixesSelectCursorInput(display, root, XFixesDisplayCursorNotifyMask); |
| 626 | g_timeout_add(4, x_tick, NULL); |
| 627 | } |
| 628 | login_bus = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, NULL); |
| 629 | if (login_bus) { |
| 630 | GVariant *reply = g_dbus_connection_call_sync(login_bus, "org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", "GetUser", |
| 631 | g_variant_new("(u)", (guint)getuid()), G_VARIANT_TYPE("(o)"), G_DBUS_CALL_FLAGS_NONE, 2000, NULL, NULL); |
| 632 | if (reply) { |
| 633 | const char *user_path; g_variant_get(reply, "(&o)", &user_path); |
| 634 | GVariant *property = g_dbus_connection_call_sync(login_bus, "org.freedesktop.login1", user_path, "org.freedesktop.DBus.Properties", "Get", |
| 635 | g_variant_new("(ss)", "org.freedesktop.login1.User", "Display"), G_VARIANT_TYPE("(v)"), G_DBUS_CALL_FLAGS_NONE, 2000, NULL, NULL); |
| 636 | g_variant_unref(reply); reply = NULL; |
| 637 | if (property) { |
| 638 | GVariant *value; g_variant_get(property, "(v)", &value); |
| 639 | const char *id, *path; g_variant_get(value, "(&s&o)", &id, &path); |
| 640 | if (*id) reply = g_variant_ref_sink(g_variant_new("(o)", path)); |
| 641 | g_variant_unref(value); g_variant_unref(property); |
| 642 | } |
| 643 | } |
| 644 | if (reply) { |
| 645 | const char *path; g_variant_get(reply, "(&o)", &path); |
| 646 | g_dbus_connection_signal_subscribe(login_bus, "org.freedesktop.login1", "org.freedesktop.DBus.Properties", "PropertiesChanged", path, NULL, G_DBUS_SIGNAL_FLAGS_NONE, login_changed, NULL, NULL); |
| 647 | GVariant *property = g_dbus_connection_call_sync(login_bus, "org.freedesktop.login1", path, "org.freedesktop.DBus.Properties", "GetAll", |
| 648 | g_variant_new("(s)", "org.freedesktop.login1.Session"), G_VARIANT_TYPE("(a{sv})"), G_DBUS_CALL_FLAGS_NONE, 2000, NULL, NULL); |
| 649 | g_variant_unref(reply); |
| 650 | if (property) { GVariant *properties; g_variant_get(property, "(@a{sv})", &properties); login_state(properties); g_variant_unref(properties); g_variant_unref(property); } |
| 651 | } |
| 652 | } |
| 653 | g_unix_signal_add(SIGTERM, quit, NULL); g_unix_signal_add(SIGINT, quit, NULL); |
| 654 | g_timeout_add(1000, heartbeat, NULL); heartbeat(NULL); |
| 655 | g_main_loop_run(main_loop); |
| 656 | cleanup: |
| 657 | close_channel(); |
| 658 | if (display) { free_ximage(); XCloseDisplay(display); } |
| 659 | if (pw_watch) g_source_remove(pw_watch); |
| 660 | if (pw_stream) pw_stream_destroy(pw_stream); |
| 661 | if (pw_core) pw_core_disconnect(pw_core); |
| 662 | if (pw_context) pw_context_destroy(pw_context); |
| 663 | if (pw_loop) { pw_loop_leave(pw_main_loop_get_loop(pw_loop)); pw_main_loop_destroy(pw_loop); } |
| 664 | if (bus) { |
| 665 | if (session_path) g_dbus_connection_call_sync(bus, "org.freedesktop.portal.Desktop", session_path, "org.freedesktop.portal.Session", "Close", NULL, NULL, G_DBUS_CALL_FLAGS_NONE, 2000, NULL, NULL); |
| 666 | if (session_subscription) g_dbus_connection_signal_unsubscribe(bus, session_subscription); |
| 667 | if (portal_subscription) g_dbus_connection_signal_unsubscribe(bus, portal_subscription); |
| 668 | g_object_unref(bus); |
| 669 | } |
| 670 | if (login_bus) g_object_unref(login_bus); |
| 671 | g_free(session_path); g_free(request_path); g_free(restore_path); free(before); |
| 672 | g_byte_array_unref(out); g_byte_array_unref(cursor_packet); g_main_loop_unref(main_loop); pw_deinit(); |
| 673 | if (restart) { |
| 674 | signal(SIGTERM, SIG_DFL); signal(SIGINT, SIG_DFL); |
| 675 | g_usleep(5000000); |
| 676 | execvp(argv[0], argv); |
| 677 | return 1; |
| 678 | } |
| 679 | return 0; |
| 680 | } |