diff --git a/.gitignore b/.gitignore index 403d243ee..a4c4a7468 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ build-*/ .lvimrc config-debug wayland-*-protocol.* +__pycache__ +.wraplock +.pytest_cache/ +.ruff_cache/ + diff --git a/include/khashl.h b/include/khashl.h new file mode 100644 index 000000000..4e05416c8 --- /dev/null +++ b/include/khashl.h @@ -0,0 +1,506 @@ +/* The MIT License + + Copyright (c) 2019- by Attractive Chaos + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#ifndef __AC_KHASHL_H +#define __AC_KHASHL_H + +#define AC_VERSION_KHASHL_H "r40" + +#include +#include +#include + +/************************************ + * Compiler specific configurations * + ************************************/ + +#if UINT_MAX == 0xffffffffu +typedef unsigned int khint32_t; +#elif ULONG_MAX == 0xffffffffu +typedef unsigned long khint32_t; +#endif + +#if ULONG_MAX == ULLONG_MAX +typedef unsigned long khint64_t; +#else +typedef unsigned long long khint64_t; +#endif + +#ifndef kh_inline +#ifdef _MSC_VER +#define kh_inline __inline +#else +#define kh_inline inline +#endif +#endif /* kh_inline */ + +#ifndef klib_unused +#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3) +#define klib_unused __attribute__ ((__unused__)) +#else +#define klib_unused +#endif +#endif /* klib_unused */ + +#define KH_LOCAL static kh_inline klib_unused + +typedef khint32_t khint_t; +typedef const char *kh_cstr_t; + +/*********************** + * Configurable macros * + ***********************/ + +#ifndef kh_max_count /* set the max load factor */ +#define kh_max_count(cap) (((cap)>>1) + ((cap)>>2)) /* default load factor: 75% */ +#endif + +#ifndef kh_packed /* pack the key-value struct */ +#define kh_packed __attribute__ ((__packed__)) +#endif + +#if !defined(Kmalloc) || !defined(Kcalloc) || !defined(Krealloc) || !defined(Kfree) +#define Kmalloc(km, type, cnt) ((type*)malloc((cnt) * sizeof(type))) +#define Kcalloc(km, type, cnt) ((type*)calloc((cnt), sizeof(type))) +#define Krealloc(km, type, ptr, cnt) ((type*)realloc((ptr), (cnt) * sizeof(type))) +#define Kfree(km, ptr) free(ptr) +#endif + +/**************************** + * Simple private functions * + ****************************/ + +#define __kh_used(flag, i) (flag[i>>5] >> (i&0x1fU) & 1U) +#define __kh_set_used(flag, i) (flag[i>>5] |= 1U<<(i&0x1fU)) +#define __kh_set_unused(flag, i) (flag[i>>5] &= ~(1U<<(i&0x1fU))) + +#define __kh_fsize(m) ((m) < 32? 1 : (m)>>5) + +static kh_inline khint_t __kh_splitmix32(khint_t *x) { khint_t z = (*x += 0x9e3779b9U); z = (z ^ (z >> 16)) * 0x21f0aaadU; z = (z ^ (z >> 15)) * 0x735a2d97U; return z ^ (z >> 15); } +static kh_inline khint_t __kh_h2b(khint_t hash, khint_t salt, khint_t bits) { return (hash ^ salt) * 2654435769U >> (32 - bits); } /* Fibonacci hashing */ + +/******************* + * Hash table base * + *******************/ + +#define __KHASHL_TYPE(HType, khkey_t) \ + typedef struct HType { \ + void *km; \ + unsigned short bits, salt; \ + khint_t count; \ + khint32_t *used; \ + khkey_t *keys; \ + } HType; + +#define __KHASHL_PROTOTYPES(HType, prefix, khkey_t) \ + extern HType *prefix##_init(void); \ + extern HType *prefix##_init2(void *km); \ + extern void prefix##_destroy(HType *h); \ + extern void prefix##_clear(HType *h); \ + extern khint_t prefix##_getp(const HType *h, const khkey_t *key); \ + extern int prefix##_resize(HType *h, khint_t new_n_buckets); \ + extern khint_t prefix##_putp(HType *h, const khkey_t *key, int *absent); \ + extern void prefix##_del(HType *h, khint_t k); + +#define __KHASHL_IMPL_BASIC(SCOPE, HType, prefix) \ + SCOPE HType *prefix##_init3(void *km, khint_t seed) { \ + HType *h = Kcalloc(km, HType, 1); \ + h->km = km; \ + if (seed != 0) h->salt = __kh_splitmix32(&seed); \ + return h; \ + } \ + SCOPE HType *prefix##_init2(void *km) { return prefix##_init3(km, 0); } \ + SCOPE HType *prefix##_init(void) { return prefix##_init2(0); } \ + SCOPE void prefix##_destroy(HType *h) { \ + if (!h) return; \ + Kfree(h->km, (void*)h->keys); Kfree(h->km, h->used); \ + Kfree(h->km, h); \ + } \ + SCOPE void prefix##_clear(HType *h) { \ + if (h && h->used) { \ + khint_t n_buckets = (khint_t)1U << h->bits; \ + memset(h->used, 0, __kh_fsize(n_buckets) * sizeof(khint32_t)); \ + h->count = 0; \ + } \ + } + +#define __KHASHL_IMPL_GET(SCOPE, HType, prefix, khkey_t, __hash_fn, __hash_eq) \ + SCOPE khint_t prefix##_getp_core(const HType *h, const khkey_t *key, khint_t hash) { \ + khint_t i, last, n_buckets, mask; \ + if (h->keys == 0) return 0; \ + n_buckets = (khint_t)1U << h->bits; \ + mask = n_buckets - 1U; \ + i = last = __kh_h2b(hash, h->salt, h->bits); \ + while (__kh_used(h->used, i) && !__hash_eq(h->keys[i], *key)) { \ + i = (i + 1U) & mask; \ + if (i == last) return n_buckets; \ + } \ + return !__kh_used(h->used, i)? n_buckets : i; \ + } \ + SCOPE khint_t prefix##_getp(const HType *h, const khkey_t *key) { return prefix##_getp_core(h, key, __hash_fn(*key)); } \ + SCOPE khint_t prefix##_get(const HType *h, khkey_t key) { return prefix##_getp_core(h, &key, __hash_fn(key)); } + +#define __KHASHL_IMPL_RESIZE(SCOPE, HType, prefix, khkey_t, __hash_fn, __hash_eq) \ + SCOPE int prefix##_resize(HType *h, khint_t new_n_buckets) { \ + khint32_t *new_used = 0; \ + khint_t j = 0, x = new_n_buckets, n_buckets, new_bits, new_mask; \ + while ((x >>= 1) != 0) ++j; \ + if (new_n_buckets & (new_n_buckets - 1)) ++j; \ + new_bits = j > 2? j : 2; \ + if (new_bits == h->bits) return 0; /* same size; no need to rehash */ \ + new_n_buckets = (khint_t)1U << new_bits; \ + if (h->count > kh_max_count(new_n_buckets)) return 0; /* requested size is too small */ \ + new_used = Kmalloc(h->km, khint32_t, __kh_fsize(new_n_buckets)); \ + if (!new_used) return -1; /* not enough memory */ \ + memset(new_used, 0, __kh_fsize(new_n_buckets) * sizeof(khint32_t)); \ + n_buckets = h->keys? (khint_t)1U<bits : 0U; \ + if (n_buckets < new_n_buckets) { /* expand */ \ + khkey_t *new_keys = Krealloc(h->km, khkey_t, h->keys, new_n_buckets); \ + if (!new_keys) { Kfree(h->km, new_used); return -1; } \ + h->keys = new_keys; \ + } \ + new_mask = new_n_buckets - 1; \ + for (j = 0; j != n_buckets; ++j) { \ + khkey_t key; \ + if (!__kh_used(h->used, j)) continue; \ + key = h->keys[j]; \ + __kh_set_unused(h->used, j); \ + while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \ + khint_t i; \ + i = __kh_h2b(__hash_fn(key), h->salt, new_bits); \ + while (__kh_used(new_used, i)) i = (i + 1U) & new_mask; \ + __kh_set_used(new_used, i); \ + if (i < n_buckets && __kh_used(h->used, i)) { /* kick out the existing element */ \ + { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \ + __kh_set_unused(h->used, i); /* mark it as deleted in the old hash table */ \ + } else { /* write the element and jump out of the loop */ \ + h->keys[i] = key; \ + break; \ + } \ + } \ + } \ + if (n_buckets > new_n_buckets) { /* shrink the hash table */ \ + khkey_t *new_keys = Krealloc(h->km, khkey_t, h->keys, new_n_buckets); \ + if (!new_keys) { Kfree(h->km, new_used); return -1; } \ + h->keys = new_keys; \ + } \ + Kfree(h->km, h->used); /* free the working space */ \ + h->used = new_used, h->bits = new_bits; \ + return 0; \ + } + +#define __KHASHL_IMPL_PUT(SCOPE, HType, prefix, khkey_t, __hash_fn, __hash_eq) \ + SCOPE khint_t prefix##_putp_core(HType *h, const khkey_t *key, khint_t hash, int *absent) { \ + khint_t n_buckets, i, last, mask; \ + n_buckets = h->keys? (khint_t)1U<bits : 0U; \ + *absent = -1; \ + if (h->count >= kh_max_count(n_buckets)) { /* rehashing */ \ + if (prefix##_resize(h, n_buckets + 1U) < 0) \ + return n_buckets; \ + n_buckets = (khint_t)1U<bits; \ + } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \ + mask = n_buckets - 1; \ + i = last = __kh_h2b(hash, h->salt, h->bits); \ + while (__kh_used(h->used, i) && !__hash_eq(h->keys[i], *key)) { \ + i = (i + 1U) & mask; \ + if (i == last) break; \ + } \ + if (!__kh_used(h->used, i)) { /* not present at all */ \ + h->keys[i] = *key; \ + __kh_set_used(h->used, i); \ + ++h->count; \ + *absent = 1; \ + } else *absent = 0; /* Don't touch h->keys[i] if present */ \ + return i; \ + } \ + SCOPE khint_t prefix##_putp(HType *h, const khkey_t *key, int *absent) { return prefix##_putp_core(h, key, __hash_fn(*key), absent); } \ + SCOPE khint_t prefix##_put(HType *h, khkey_t key, int *absent) { return prefix##_putp_core(h, &key, __hash_fn(key), absent); } + +#define __KHASHL_IMPL_DEL(SCOPE, HType, prefix, khkey_t, __hash_fn) \ + SCOPE int prefix##_del(HType *h, khint_t i) { \ + khint_t j = i, k, mask, n_buckets; \ + if (h->keys == 0) return 0; \ + n_buckets = (khint_t)1U<bits; \ + mask = n_buckets - 1U; \ + while (1) { \ + j = (j + 1U) & mask; \ + if (j == i || !__kh_used(h->used, j)) break; /* j==i only when the table is completely full */ \ + k = __kh_h2b(__hash_fn(h->keys[j]), h->salt, h->bits); \ + if ((j > i && (k <= i || k > j)) || (j < i && (k <= i && k > j))) \ + h->keys[i] = h->keys[j], i = j; \ + } \ + __kh_set_unused(h->used, i); \ + --h->count; \ + return 1; \ + } + +#define KHASHL_DECLARE(HType, prefix, khkey_t) \ + __KHASHL_TYPE(HType, khkey_t) \ + __KHASHL_PROTOTYPES(HType, prefix, khkey_t) + +#define KHASHL_INIT(SCOPE, HType, prefix, khkey_t, __hash_fn, __hash_eq) \ + __KHASHL_TYPE(HType, khkey_t) \ + __KHASHL_IMPL_BASIC(SCOPE, HType, prefix) \ + __KHASHL_IMPL_GET(SCOPE, HType, prefix, khkey_t, __hash_fn, __hash_eq) \ + __KHASHL_IMPL_RESIZE(SCOPE, HType, prefix, khkey_t, __hash_fn, __hash_eq) \ + __KHASHL_IMPL_PUT(SCOPE, HType, prefix, khkey_t, __hash_fn, __hash_eq) \ + __KHASHL_IMPL_DEL(SCOPE, HType, prefix, khkey_t, __hash_fn) + +/*************************** + * Ensemble of hash tables * + ***************************/ + +typedef struct { + khint_t sub, pos; +} kh_ensitr_t; + +#define KHASHE_INIT(SCOPE, HType, prefix, khkey_t, __hash_fn, __hash_eq) \ + KHASHL_INIT(KH_LOCAL, HType##_sub, prefix##_sub, khkey_t, __hash_fn, __hash_eq) \ + typedef struct HType { \ + void *km; \ + khint64_t count:54, bits:8; \ + HType##_sub *sub; \ + } HType; \ + SCOPE HType *prefix##_init3(void *km, int bits, khint_t seed) { \ + HType *g; \ + g = Kcalloc(km, HType, 1); \ + if (!g) return 0; \ + g->bits = bits, g->km = km; \ + g->sub = Kcalloc(km, HType##_sub, 1U<sub[i].salt = __kh_splitmix32(&rng); \ + } \ + return g; \ + } \ + SCOPE HType *prefix##_init2(void *km, int bits) { return prefix##_init3(km, bits, 0); } \ + SCOPE HType *prefix##_init(int bits) { return prefix##_init2(0, bits); } \ + SCOPE void prefix##_destroy(HType *g) { \ + int t; \ + if (!g) return; \ + for (t = 0; t < 1<bits; ++t) { Kfree(g->km, (void*)g->sub[t].keys); Kfree(g->km, g->sub[t].used); } \ + Kfree(g->km, g->sub); Kfree(g->km, g); \ + } \ + SCOPE kh_ensitr_t prefix##_getp(const HType *g, const khkey_t *key) { \ + khint_t hash, low, ret; \ + kh_ensitr_t r; \ + HType##_sub *h; \ + hash = __hash_fn(*key); \ + low = hash & ((1U<bits) - 1); \ + h = &g->sub[low]; \ + ret = prefix##_sub_getp_core(h, key, hash); \ + if (ret == kh_end(h)) r.sub = low, r.pos = (khint_t)-1; \ + else r.sub = low, r.pos = ret; \ + return r; \ + } \ + SCOPE kh_ensitr_t prefix##_get(const HType *g, const khkey_t key) { return prefix##_getp(g, &key); } \ + SCOPE kh_ensitr_t prefix##_putp(HType *g, const khkey_t *key, int *absent) { \ + khint_t hash, low, ret; \ + kh_ensitr_t r; \ + HType##_sub *h; \ + hash = __hash_fn(*key); \ + low = hash & ((1U<bits) - 1); \ + h = &g->sub[low]; \ + ret = prefix##_sub_putp_core(h, key, hash, absent); \ + if (*absent) ++g->count; \ + r.sub = low, r.pos = ret; \ + return r; \ + } \ + SCOPE kh_ensitr_t prefix##_put(HType *g, const khkey_t key, int *absent) { return prefix##_putp(g, &key, absent); } \ + SCOPE int prefix##_del(HType *g, kh_ensitr_t itr) { \ + HType##_sub *h = &g->sub[itr.sub]; \ + int ret; \ + ret = prefix##_sub_del(h, itr.pos); \ + if (ret) --g->count; \ + return ret; \ + } \ + SCOPE void prefix##_clear(HType *g) { \ + int i; \ + for (i = 0; i < 1U<bits; ++i) prefix##_sub_clear(&g->sub[i]); \ + g->count = 0; \ + } \ + SCOPE void prefix##_resize(HType *g, khint64_t new_n_buckets) { \ + khint_t j; \ + for (j = 0; j < 1U<bits; ++j) \ + prefix##_sub_resize(&g->sub[j], new_n_buckets >> g->bits); \ + } + +/***************************** + * More convenient interface * + *****************************/ + +/* common */ + +#define KHASHL_SET_INIT(SCOPE, HType, prefix, khkey_t, __hash_fn, __hash_eq) \ + typedef struct { khkey_t key; } kh_packed HType##_s_bucket_t; \ + static kh_inline khint_t prefix##_s_hash(HType##_s_bucket_t x) { return __hash_fn(x.key); } \ + static kh_inline int prefix##_s_eq(HType##_s_bucket_t x, HType##_s_bucket_t y) { return __hash_eq(x.key, y.key); } \ + KHASHL_INIT(KH_LOCAL, HType, prefix##_s, HType##_s_bucket_t, prefix##_s_hash, prefix##_s_eq) \ + SCOPE HType *prefix##_init(void) { return prefix##_s_init(); } \ + SCOPE HType *prefix##_init2(void *km) { return prefix##_s_init2(km); } \ + SCOPE HType *prefix##_init3(void *km, khint_t seed) { return prefix##_s_init3(km, seed); } \ + SCOPE void prefix##_destroy(HType *h) { prefix##_s_destroy(h); } \ + SCOPE void prefix##_resize(HType *h, khint_t new_n_buckets) { prefix##_s_resize(h, new_n_buckets); } \ + SCOPE khint_t prefix##_get(const HType *h, khkey_t key) { HType##_s_bucket_t t; t.key = key; return prefix##_s_getp(h, &t); } \ + SCOPE int prefix##_del(HType *h, khint_t k) { return prefix##_s_del(h, k); } \ + SCOPE khint_t prefix##_put(HType *h, khkey_t key, int *absent) { HType##_s_bucket_t t; t.key = key; return prefix##_s_putp(h, &t, absent); } \ + SCOPE void prefix##_clear(HType *h) { prefix##_s_clear(h); } + +#define KHASHL_MAP_INIT(SCOPE, HType, prefix, khkey_t, kh_val_t, __hash_fn, __hash_eq) \ + typedef struct { khkey_t key; kh_val_t val; } kh_packed HType##_m_bucket_t; \ + static kh_inline khint_t prefix##_m_hash(HType##_m_bucket_t x) { return __hash_fn(x.key); } \ + static kh_inline int prefix##_m_eq(HType##_m_bucket_t x, HType##_m_bucket_t y) { return __hash_eq(x.key, y.key); } \ + KHASHL_INIT(KH_LOCAL, HType, prefix##_m, HType##_m_bucket_t, prefix##_m_hash, prefix##_m_eq) \ + SCOPE HType *prefix##_init(void) { return prefix##_m_init(); } \ + SCOPE HType *prefix##_init2(void *km) { return prefix##_m_init2(km); } \ + SCOPE HType *prefix##_init3(void *km, khint_t seed) { return prefix##_m_init3(km, seed); } \ + SCOPE void prefix##_destroy(HType *h) { prefix##_m_destroy(h); } \ + SCOPE void prefix##_resize(HType *h, khint_t new_n_buckets) { prefix##_m_resize(h, new_n_buckets); } \ + SCOPE khint_t prefix##_get(const HType *h, khkey_t key) { HType##_m_bucket_t t; t.key = key; return prefix##_m_getp(h, &t); } \ + SCOPE int prefix##_del(HType *h, khint_t k) { return prefix##_m_del(h, k); } \ + SCOPE khint_t prefix##_put(HType *h, khkey_t key, int *absent) { HType##_m_bucket_t t; t.key = key; return prefix##_m_putp(h, &t, absent); } \ + SCOPE void prefix##_clear(HType *h) { prefix##_m_clear(h); } + +/* cached hashes to trade memory for performance when hashing and comparison are expensive */ + +#define __kh_cached_hash(x) ((x).hash) + +#define KHASHL_CSET_INIT(SCOPE, HType, prefix, khkey_t, __hash_fn, __hash_eq) \ + typedef struct { khkey_t key; khint_t hash; } kh_packed HType##_cs_bucket_t; \ + static kh_inline int prefix##_cs_eq(HType##_cs_bucket_t x, HType##_cs_bucket_t y) { return x.hash == y.hash && __hash_eq(x.key, y.key); } \ + KHASHL_INIT(KH_LOCAL, HType, prefix##_cs, HType##_cs_bucket_t, __kh_cached_hash, prefix##_cs_eq) \ + SCOPE HType *prefix##_init(void) { return prefix##_cs_init(); } \ + SCOPE void prefix##_destroy(HType *h) { prefix##_cs_destroy(h); } \ + SCOPE khint_t prefix##_get(const HType *h, khkey_t key) { HType##_cs_bucket_t t; t.key = key; t.hash = __hash_fn(key); return prefix##_cs_getp(h, &t); } \ + SCOPE int prefix##_del(HType *h, khint_t k) { return prefix##_cs_del(h, k); } \ + SCOPE khint_t prefix##_put(HType *h, khkey_t key, int *absent) { HType##_cs_bucket_t t; t.key = key, t.hash = __hash_fn(key); return prefix##_cs_putp(h, &t, absent); } \ + SCOPE void prefix##_clear(HType *h) { prefix##_cs_clear(h); } + +#define KHASHL_CMAP_INIT(SCOPE, HType, prefix, khkey_t, kh_val_t, __hash_fn, __hash_eq) \ + typedef struct { khkey_t key; kh_val_t val; khint_t hash; } kh_packed HType##_cm_bucket_t; \ + static kh_inline int prefix##_cm_eq(HType##_cm_bucket_t x, HType##_cm_bucket_t y) { return x.hash == y.hash && __hash_eq(x.key, y.key); } \ + KHASHL_INIT(KH_LOCAL, HType, prefix##_cm, HType##_cm_bucket_t, __kh_cached_hash, prefix##_cm_eq) \ + SCOPE HType *prefix##_init(void) { return prefix##_cm_init(); } \ + SCOPE void prefix##_destroy(HType *h) { prefix##_cm_destroy(h); } \ + SCOPE khint_t prefix##_get(const HType *h, khkey_t key) { HType##_cm_bucket_t t; t.key = key; t.hash = __hash_fn(key); return prefix##_cm_getp(h, &t); } \ + SCOPE int prefix##_del(HType *h, khint_t k) { return prefix##_cm_del(h, k); } \ + SCOPE khint_t prefix##_put(HType *h, khkey_t key, int *absent) { HType##_cm_bucket_t t; t.key = key, t.hash = __hash_fn(key); return prefix##_cm_putp(h, &t, absent); } \ + SCOPE void prefix##_clear(HType *h) { prefix##_cm_clear(h); } + +/* ensemble for huge hash tables */ + +#define KHASHE_SET_INIT(SCOPE, HType, prefix, khkey_t, __hash_fn, __hash_eq) \ + typedef struct { khkey_t key; } kh_packed HType##_es_bucket_t; \ + static kh_inline khint_t prefix##_es_hash(HType##_es_bucket_t x) { return __hash_fn(x.key); } \ + static kh_inline int prefix##_es_eq(HType##_es_bucket_t x, HType##_es_bucket_t y) { return __hash_eq(x.key, y.key); } \ + KHASHE_INIT(KH_LOCAL, HType, prefix##_es, HType##_es_bucket_t, prefix##_es_hash, prefix##_es_eq) \ + SCOPE HType *prefix##_init(int bits) { return prefix##_es_init(bits); } \ + SCOPE void prefix##_destroy(HType *h) { prefix##_es_destroy(h); } \ + SCOPE void prefix##_resize(HType *h, khint64_t new_n_buckets) { prefix##_es_resize(h, new_n_buckets); } \ + SCOPE kh_ensitr_t prefix##_get(const HType *h, khkey_t key) { HType##_es_bucket_t t; t.key = key; return prefix##_es_getp(h, &t); } \ + SCOPE int prefix##_del(HType *h, kh_ensitr_t k) { return prefix##_es_del(h, k); } \ + SCOPE kh_ensitr_t prefix##_put(HType *h, khkey_t key, int *absent) { HType##_es_bucket_t t; t.key = key; return prefix##_es_putp(h, &t, absent); } \ + SCOPE void prefix##_clear(HType *h) { prefix##_es_clear(h); } + +#define KHASHE_MAP_INIT(SCOPE, HType, prefix, khkey_t, kh_val_t, __hash_fn, __hash_eq) \ + typedef struct { khkey_t key; kh_val_t val; } kh_packed HType##_em_bucket_t; \ + static kh_inline khint_t prefix##_em_hash(HType##_em_bucket_t x) { return __hash_fn(x.key); } \ + static kh_inline int prefix##_em_eq(HType##_em_bucket_t x, HType##_em_bucket_t y) { return __hash_eq(x.key, y.key); } \ + KHASHE_INIT(KH_LOCAL, HType, prefix##_em, HType##_em_bucket_t, prefix##_em_hash, prefix##_em_eq) \ + SCOPE HType *prefix##_init(int bits) { return prefix##_em_init(bits); } \ + SCOPE void prefix##_destroy(HType *h) { prefix##_em_destroy(h); } \ + SCOPE void prefix##_resize(HType *h, khint64_t new_n_buckets) { prefix##_em_resize(h, new_n_buckets); } \ + SCOPE kh_ensitr_t prefix##_get(const HType *h, khkey_t key) { HType##_em_bucket_t t; t.key = key; return prefix##_em_getp(h, &t); } \ + SCOPE int prefix##_del(HType *h, kh_ensitr_t k) { return prefix##_em_del(h, k); } \ + SCOPE kh_ensitr_t prefix##_put(HType *h, khkey_t key, int *absent) { HType##_em_bucket_t t; t.key = key; return prefix##_em_putp(h, &t, absent); } \ + SCOPE void prefix##_clear(HType *h) { prefix##_em_clear(h); } + +/************************** + * Public macro functions * + **************************/ + +#define kh_bucket(h, x) ((h)->keys[x]) +#define kh_size(h) ((h)->count) +#define kh_capacity(h) ((h)->keys? 1U<<(h)->bits : 0U) +#define kh_end(h) kh_capacity(h) + +#define kh_key(h, x) ((h)->keys[x].key) +#define kh_val(h, x) ((h)->keys[x].val) +#define kh_exist(h, x) __kh_used((h)->used, (x)) + +#define kh_foreach(h, x) for ((x) = 0; (x) != kh_end(h); ++(x)) if (kh_exist((h), (x))) + +#define kh_ens_key(g, x) kh_key(&(g)->sub[(x).sub], (x).pos) +#define kh_ens_val(g, x) kh_val(&(g)->sub[(x).sub], (x).pos) +#define kh_ens_exist(g, x) kh_exist(&(g)->sub[(x).sub], (x).pos) +#define kh_ens_is_end(x) ((x).pos == (khint_t)-1) +#define kh_ens_size(g) ((g)->count) + +#define kh_ens_foreach(g, x) for ((x).sub = 0; (x).sub != 1<<(g)->bits; ++(x).sub) for ((x).pos = 0; (x).pos != kh_end(&(g)->sub[(x).sub]); ++(x).pos) if (kh_ens_exist((g), (x))) + +/************************************** + * Common hash and equality functions * + **************************************/ + +#define kh_eq_generic(a, b) ((a) == (b)) +#define kh_eq_str(a, b) (strcmp((a), (b)) == 0) +#define kh_hash_dummy(x) ((khint_t)(x)) + +static kh_inline khint_t kh_hash_uint32(khint_t x) { /* murmur finishing */ + x ^= x >> 16; + x *= 0x85ebca6bU; + x ^= x >> 13; + x *= 0xc2b2ae35U; + x ^= x >> 16; + return x; +} + +static kh_inline khint_t kh_hash_uint64(khint64_t x) { /* splitmix64; see https://nullprogram.com/blog/2018/07/31/ for inversion */ + x ^= x >> 30; + x *= 0xbf58476d1ce4e5b9ULL; + x ^= x >> 27; + x *= 0x94d049bb133111ebULL; + x ^= x >> 31; + return (khint_t)x; +} + +static kh_inline khint_t kh_hash_str(kh_cstr_t s) { /* FNV1a */ + khint_t h = 2166136261U; + const unsigned char *t = (const unsigned char*)s; + for (; *t; ++t) + h ^= *t, h *= 16777619; + return h; +} + +static kh_inline khint_t kh_hash_bytes(int len, const unsigned char *s) { + khint_t h = 2166136261U; + int i; + for (i = 0; i < len; ++i) + h ^= s[i], h *= 16777619; + return h; +} + +#endif /* __AC_KHASHL_H */ diff --git a/include/sway/desktop/transaction.h b/include/sway/desktop/transaction.h index be9252312..805bd3820 100644 --- a/include/sway/desktop/transaction.h +++ b/include/sway/desktop/transaction.h @@ -23,6 +23,7 @@ struct sway_transaction_instruction; struct sway_view; +struct sway_server; /** * Find all dirty containers, create and commit a transaction containing them, @@ -71,4 +72,6 @@ void arrange_popups(struct wlr_scene_tree *popups); */ void config_default_animation_callbacks(); +void transaction_shut_down(struct sway_server *server); + #endif diff --git a/include/sway/input/input-manager.h b/include/sway/input/input-manager.h index 05561a880..0aad7b70a 100644 --- a/include/sway/input/input-manager.h +++ b/include/sway/input/input-manager.h @@ -41,6 +41,7 @@ struct sway_input_manager { struct sway_input_manager *input_manager_create(struct sway_server *server); void input_manager_finish(struct sway_input_manager *input); +void input_manager_destroy(struct sway_input_manager *input); bool input_manager_has_focus(struct sway_node *node); diff --git a/include/sway/tree/node.h b/include/sway/tree/node.h index 4e4b4cc82..ac486153d 100644 --- a/include/sway/tree/node.h +++ b/include/sway/tree/node.h @@ -71,6 +71,7 @@ const char *node_type_to_str(enum sway_node_type type); * next transaction then unmarked as dirty. */ void node_set_dirty(struct sway_node *node); +void node_clear_dirty(struct sway_node *node); bool node_is_view(struct sway_node *node); @@ -106,4 +107,10 @@ void node_set_focus_warp(struct sway_node *node, enum sway_node_focus_warp warp) enum sway_node_focus_warp node_get_focus_warp(struct sway_node *node); +void node_fini(struct sway_node *node); +struct sway_node *node_by_id(size_t id); +void node_map_init(void); +void node_map_fini(void); + #endif + diff --git a/include/sway/tree/space.h b/include/sway/tree/space.h index 6ed3ffe8d..217a5f7ab 100644 --- a/include/sway/tree/space.h +++ b/include/sway/tree/space.h @@ -10,7 +10,7 @@ enum sway_space_restore { }; struct sway_space_view { - struct sway_view *view; + size_t container_id; struct sway_space_container *container; float content_scale; }; diff --git a/include/swaybar/tray/host.h b/include/swaybar/tray/host.h index 2d4cf82b1..fa6e22c2f 100644 --- a/include/swaybar/tray/host.h +++ b/include/swaybar/tray/host.h @@ -2,6 +2,14 @@ #define _SWAYBAR_TRAY_HOST_H #include +#include "config.h" +#if HAVE_LIBSYSTEMD +#include +#elif HAVE_LIBELOGIND +#include +#elif HAVE_BASU +#include +#endif struct swaybar_tray; @@ -9,6 +17,9 @@ struct swaybar_host { struct swaybar_tray *tray; char *service; char *watcher_interface; + sd_bus_slot *reg_slot; + sd_bus_slot *unreg_slot; + sd_bus_slot *watcher_slot; }; bool init_host(struct swaybar_host *host, char *protocol, struct swaybar_tray *tray); diff --git a/include/swaybar/tray/watcher.h b/include/swaybar/tray/watcher.h index 8f276da8e..50393c909 100644 --- a/include/swaybar/tray/watcher.h +++ b/include/swaybar/tray/watcher.h @@ -10,6 +10,8 @@ struct swaybar_watcher { list_t *hosts; list_t *items; int version; + sd_bus_slot *vtable_slot; + sd_bus_slot *signal_slot; }; struct swaybar_watcher *create_watcher(char *protocol, sd_bus *bus); diff --git a/meson.build b/meson.build index 7222d0d37..cc0402b91 100644 --- a/meson.build +++ b/meson.build @@ -68,6 +68,7 @@ wayland_cursor = dependency('wayland-cursor') wayland_protos = dependency('wayland-protocols', version: '>=1.47', default_options: ['tests=false']) xkbcommon = dependency('xkbcommon', version: '>=1.5.0') cairo = dependency('cairo') +fontconfig = dependency('fontconfig') pango = dependency('pango') pangocairo = dependency('pangocairo') gdk_pixbuf = dependency('gdk-pixbuf-2.0', required: get_option('gdk-pixbuf')) @@ -246,6 +247,7 @@ if get_option('default-wallpaper') endif subdir('completions') +subdir('tests') summary({ 'gdk-pixbuf': gdk_pixbuf.found(), diff --git a/scroll.lua b/scroll.lua index efe2137c8..ab7463f7d 100644 --- a/scroll.lua +++ b/scroll.lua @@ -49,56 +49,66 @@ function scroll.ipc_send(id, data) end --- --- Returns all the results/errors in an array. --- ---- @param context userdata|nil +--- @param context integer|nil --- @param command string --- --- @return string[] function scroll.command(context, command) end --- ---- Returns the focused view pointer or nil if none. +--- Returns the type of the given node as a string. +--- Possible values are: "root", "output", "workspace", "container", "layer_surface", "layer_popup". +--- Returns nil if the node ID is invalid or not found. --- ---- @return userdata|nil +--- @param node integer +--- +--- @return string|nil +function scroll.node_get_type(node) end + +--- +--- Returns the focused view ID or nil if none. +--- +--- @return integer|nil function scroll.focused_view() end --- ---- Returns the focused container pointer or nil if none. +--- Returns the focused container ID or nil if none. --- ---- @return userdata|nil +--- @return integer|nil function scroll.focused_container() end --- ---- Returns the focused workspace pointer or nil if none. +--- Returns the focused workspace ID or nil if none. --- ---- @return userdata|nil +--- @return integer|nil function scroll.focused_workspace() end --- ---- Returns the current urgent view pointer or nil if none. +--- Returns the current urgent view ID or nil if none. --- ---- @return userdata|nil +--- @return integer|nil function scroll.urgent_view() end --- --- Returns true if view is mapped (exists), otherwise returns false --- ---- @param view userdata +--- @param view integer --- --- @return boolean function scroll.view_mapped(view) end --- ---- Returns a pointer to the container associated to view, or nil if none. +--- Returns the container ID associated to view, or nil if none. --- ---- @param view userdata +--- @param view integer --- ---- @return userdata|nil +--- @return integer|nil function scroll.view_get_container(view) end --- --- Returns the app_id string for view, or nil if any error happens --- ---- @param view userdata +--- @param view integer --- --- @return string|nil function scroll.view_get_app_id(view) end @@ -106,7 +116,7 @@ function scroll.view_get_app_id(view) end --- --- Returns the title string for view, or nil if any error happens --- ---- @param view userdata +--- @param view integer --- --- @return string|nil function scroll.view_get_title(view) end @@ -114,7 +124,7 @@ function scroll.view_get_title(view) end --- --- Returns the pid number for view, or nil if any error happens --- ---- @param view userdata +--- @param view integer --- --- @return integer|nil function scroll.view_get_pid(view) end @@ -123,15 +133,15 @@ function scroll.view_get_pid(view) end --- Returns the view whose pid is the parent of the application running --- in view, or nil if it has no parent view. --- ---- @param view userdata +--- @param view integer --- ---- @return userdata|nil +--- @return integer|nil function scroll.view_get_parent_view(view) end --- --- If the view has the urgent attribute set, return true, otherwise false. --- ---- @param view userdata +--- @param view integer --- --- @return boolean function scroll.view_get_urgent(view) end @@ -139,7 +149,7 @@ function scroll.view_get_urgent(view) end --- --- Sets the view urgent attribute --- ---- @param view userdata +--- @param view integer --- @param urgent boolean --- --- @return integer @@ -149,7 +159,7 @@ function scroll.view_set_urgent(view, urgent) end --- Returns `xdg_shell` if the application running in view is a Wayland --- application. If it is an X windows application, returns `xwayland`. --- ---- @param view userdata +--- @param view integer --- --- @return string|nil function scroll.view_get_shell(view) end @@ -157,7 +167,7 @@ function scroll.view_get_shell(view) end --- --- Returns the tag string property for view, or nil if any error happens. --- ---- @param view userdata +--- @param view integer --- --- @return string|nil function scroll.view_get_tag(view) end @@ -165,7 +175,7 @@ function scroll.view_get_tag(view) end --- --- Close/kill view --- ---- @param view userdata +--- @param view integer --- --- @return number function scroll.view_close(view) end @@ -173,23 +183,23 @@ function scroll.view_close(view) end --- --- Sets the focus on container. --- ---- @param container userdata +--- @param container integer --- --- @return integer function scroll.container_set_focus(container) end --- ---- Returns the container's parent workspace pointer, or nil if none. +--- Returns the container's parent workspace ID, or nil if none. --- ---- @param container userdata +--- @param container integer --- ---- @return userdata|nil +--- @return integer|nil function scroll.container_get_workspace(container) end --- --- Returns an array with all the marks associated to container. --- ---- @param container userdata +--- @param container integer --- --- @return string[] function scroll.container_get_marks(container) end @@ -197,7 +207,7 @@ function scroll.container_get_marks(container) end --- --- Returns true if the container is floating, false if it is tiled. --- ---- @param container userdata +--- @param container integer --- --- @return boolean function scroll.container_get_floating(container) end @@ -205,7 +215,7 @@ function scroll.container_get_floating(container) end --- --- Returns the numerical value for the container's opacity. --- ---- @param container userdata +--- @param container integer --- --- @return number function scroll.container_get_opacity(container) end @@ -213,7 +223,7 @@ function scroll.container_get_opacity(container) end --- --- Returns true if the container is sticky, otherwise false. --- ---- @param container userdata +--- @param container integer --- --- @return boolean function scroll.container_get_sticky(container) end @@ -221,7 +231,7 @@ function scroll.container_get_sticky(container) end --- --- Returns true if the container is in the scratchpad, otherwise false. --- ---- @param container userdata +--- @param container integer --- --- @return boolean function scroll.container_get_scratchpad(container) end @@ -230,7 +240,7 @@ function scroll.container_get_scratchpad(container) end --- Returns the value for the container's width fraction. --- This value is used to compute the width of the container. --- ---- @param container userdata +--- @param container integer --- --- @return number function scroll.container_get_width_fraction(container) end @@ -239,7 +249,7 @@ function scroll.container_get_width_fraction(container) end --- Returns the value for the container's height fraction. --- This value is used to compute the height of the container. --- ---- @param container userdata +--- @param container integer --- --- @return number function scroll.container_get_height_fraction(container) end @@ -247,7 +257,7 @@ function scroll.container_get_height_fraction(container) end --- --- Returns a floating point value with the container's width in pixels. --- ---- @param container userdata +--- @param container integer --- --- @return number function scroll.container_get_width(container) end @@ -255,7 +265,7 @@ function scroll.container_get_width(container) end --- --- Returns a floating point value with the container's height in pixels. --- ---- @param container userdata +--- @param container integer --- --- @return number function scroll.container_get_height(container) end @@ -265,7 +275,7 @@ function scroll.container_get_height(container) end --- Values can be `none`, `workspace` (covers only its workspace extents) --- or `global` (covers all outputs). --- ---- @param container userdata +--- @param container integer --- --- @return string function scroll.container_get_fullscreen_mode(container) end @@ -275,7 +285,7 @@ function scroll.container_get_fullscreen_mode(container) end --- Values can be `disabled` or `enabled`. --- See the `fullscreen application` command for details. --- ---- @param container userdata +--- @param container integer --- --- @return string function scroll.container_get_fullscreen_app_mode(container) end @@ -288,7 +298,7 @@ function scroll.container_get_fullscreen_app_mode(container) end --- but this state can still be "disabled", so the compositor knows the --- container should become non-full screen when the request ends. --- ---- @param container userdata +--- @param container integer --- --- @return string function scroll.container_get_fullscreen_view_mode(container) end @@ -298,7 +308,7 @@ function scroll.container_get_fullscreen_view_mode(container) end --- Values can be `disabled` or `enabled`. --- See the `fullscreen layout` command for details. --- ---- @param container userdata +--- @param container integer --- --- @return string function scroll.container_get_fullscreen_layout_mode(container) end @@ -308,7 +318,7 @@ function scroll.container_get_fullscreen_layout_mode(container) end --- Values can be `none`, `beginning` or `end`. --- See the pin command for details. --- ---- @param container userdata +--- @param container integer --- --- @return string function scroll.container_get_pin_mode(container) end @@ -317,9 +327,9 @@ function scroll.container_get_pin_mode(container) end --- Returns the container parent of container or nil if it is a top --- level container. --- ---- @param container userdata +--- @param container integer --- ---- @return userdata|nil +--- @return integer|nil function scroll.container_get_parent(container) end --- @@ -327,9 +337,9 @@ function scroll.container_get_parent(container) end --- Only top level containers have children. Only bottom level containers --- have views. --- ---- @params container userdata +--- @params container integer --- ---- @return userdata[] +--- @return integer[] function scroll.container_get_children(container) end --- @@ -337,15 +347,15 @@ function scroll.container_get_children(container) end --- If a top level container, it will return the views of all its children, --- if a bottom level container, its only view. --- ---- @param container userdata +--- @param container integer --- ---- @return userdata[] +--- @return integer[] function scroll.container_get_views(container) end --- --- Returns an integer value with the unique container id, or nil if error. --- ---- @param container userdata +--- @param container integer --- --- @return integer|nil function scroll.container_get_id(container) end @@ -353,7 +363,7 @@ function scroll.container_get_id(container) end --- --- Sets the focus on the last active container of workspace. --- ---- @param workspace userdata +--- @param workspace integer --- --- @return integer function scroll.workspace_set_focus(workspace) end @@ -361,7 +371,7 @@ function scroll.workspace_set_focus(workspace) end --- --- Returns a string with the name of the workspace, or nil if error. --- ---- @param workspace userdata +--- @param workspace integer --- --- @return string|nil function scroll.workspace_get_name(workspace) end @@ -369,17 +379,17 @@ function scroll.workspace_get_name(workspace) end --- --- Returns an array with all the tiling containers inside the workspace. --- ---- @param workspace userdata +--- @param workspace integer --- ---- @return userdata[] +--- @return integer[] function scroll.workspace_get_tiling(workspace) end --- --- Returns an array with all the floating containers inside the workspace. --- ---- @param workspace userdata +--- @param workspace integer --- ---- @return userdata[] +--- @return integer[] function scroll.workspace_get_floating(workspace) end --- @@ -393,7 +403,7 @@ function scroll.workspace_get_floating(workspace) end --- center_horizontal: true|false --- center_vertical: true|false --- ---- @param workspace userdata +--- @param workspace integer --- --- @return table function scroll.workspace_get_mode(workspace) end @@ -410,7 +420,7 @@ function scroll.workspace_get_mode(workspace) end --- center_horizontal: true|false --- center_vertical: true|false --- ---- @param workspace userdata +--- @param workspace integer --- @param modifiers table --- --- @return integer @@ -419,7 +429,7 @@ function scroll.workspace_set_mode(workspace, modifiers) end --- --- Returns the layout type, `horizontal` or `vertical`. --- ---- @param workspace userdata +--- @param workspace integer --- --- @return string|nil function scroll.workspace_get_layout_type(workspace) end @@ -428,7 +438,7 @@ function scroll.workspace_get_layout_type(workspace) end --- Sets the workspace's layout type to layout_type, --- which can be `horizontal` or `vertical`. --- ---- @param workspace userdata +--- @param workspace integer --- @param layout_type string --- --- @return integer @@ -437,7 +447,7 @@ function scroll.workspace_set_layout_type(workspace, layout_type) end --- --- Returns an integer number with the workspace's width in pixels. --- ---- @param workspace userdata +--- @param workspace integer --- --- @return integer|nil function scroll.workspace_get_width(workspace) end @@ -445,17 +455,17 @@ function scroll.workspace_get_width(workspace) end --- --- Returns an integer number with the workspace's height in pixels. --- ---- @param workspace userdata +--- @param workspace integer --- --- @return integer|nil function scroll.workspace_get_height(workspace) end --- ---- Returns the workspace's pinned container pointer, or nil if none. +--- Returns the workspace's pinned container ID, or nil if none. --- ---- @param workspace userdata +--- @param workspace integer --- ---- @return userdata|nil +--- @return integer|nil function scroll.workspace_get_pin(workspace) end --- @@ -465,25 +475,25 @@ function scroll.workspace_get_pin(workspace) end --- split: "none"|"top"|"bottom"|"left"|"right" --- fraction: number --- gap: integer ---- sibling: the sibling workspace's pointer +--- sibling: the sibling workspace's ID --- ---- @param workspace userdata +--- @param workspace integer --- --- @return table function scroll.workspace_get_split(workspace) end --- ---- Returns the workspace's output pointer, or nil if none. +--- Returns the workspace's output ID, or nil if none. --- ---- @param workspace userdata +--- @param workspace integer --- ---- @return userdata|nil +--- @return integer|nil function scroll.workspace_get_output(workspace) end --- --- Returns true if the output (display) is enabled, or false otherwise. --- ---- @param output userdata +--- @param output integer --- --- @return boolean function scroll.output_get_enabled(output) end @@ -491,7 +501,7 @@ function scroll.output_get_enabled(output) end --- --- Returns the name of the output's interface. For example 'DP-3'. --- ---- @param output userdata +--- @param output integer --- --- @return string|nil function scroll.output_get_name(output) end @@ -499,35 +509,35 @@ function scroll.output_get_name(output) end --- --- Returns the workspace currently active on output. --- ---- @param output userdata +--- @param output integer --- ---- @return userdata|nil +--- @return integer|nil function scroll.output_get_active_workspace(output) end --- --- Returns an array with all the existing workspaces assigned to output. --- ---- @praam output userdata +--- @praam output integer --- ---- @return userdata[] +--- @return integer[] function scroll.output_get_workspaces(output) end --- --- Returns an array with all the outputs (displays). --- ---- @return userdata[] +--- @return integer[] function scroll.root_get_outputs() end --- --- Returns an array with all the containers in the scratchpad. --- ---- @return userdata[] +--- @return integer[] function scroll.scratchpad_get_containers() end --- --- Shows container if it is in the scratchpad. --- ---- @param container userdata +--- @param container integer --- --- @return integer function scroll.scratchpad_show(container) end @@ -535,7 +545,7 @@ function scroll.scratchpad_show(container) end --- --- Hide container if it is in the scratchpad. --- ---- @param container userdata +--- @param container integer --- --- @return integer function scroll.scratchpad_hide(container) end diff --git a/sway/commands/include.c b/sway/commands/include.c index e0d0c0640..94c42d123 100644 --- a/sway/commands/include.c +++ b/sway/commands/include.c @@ -9,6 +9,7 @@ struct cmd_results *cmd_include(int argc, char **argv) { char *files = join_args(argv, argc); // We don't care if the included config(s) fails to load. load_include_configs(files, config, &config->swaynag_config_errors); + free(files); return cmd_results_new(CMD_SUCCESS, NULL); } diff --git a/sway/commands/lua.c b/sway/commands/lua.c index a39a72048..fe31b8e36 100644 --- a/sway/commands/lua.c +++ b/sway/commands/lua.c @@ -1,4 +1,8 @@ +#include +#include +#include #include "sway/commands.h" +#include "log.h" static struct sway_lua_script *find_script(list_t *scripts, const char *name) { for (int i = 0; i < scripts->length; ++i) { @@ -15,16 +19,83 @@ struct cmd_results *cmd_lua(int argc, char **argv) { if ((error = checkarg(argc, "lua", EXPECTED_AT_LEAST, 1))) { return error; } - int err = luaL_loadfile(config->lua.state, argv[0]); + + char *wd = NULL; + char *config_dir = NULL; + if (config->reading && config->current_config_path) { + wd = getcwd(NULL, 0); + char *conf = strdup(config->current_config_path); + if (conf) { + config_dir = strdup(dirname(conf)); + free(conf); + } + if (config_dir && chdir(config_dir) < 0) { + sway_log(SWAY_ERROR, "failed to change working directory to config dir"); + free(config_dir); + free(wd); + return cmd_results_new(CMD_FAILURE, "Failed to change working directory to config dir"); + } + } + + char *expanded_path = NULL; + wordexp_t p; + int err_we = wordexp(argv[0], &p, 0); + + if (wd && chdir(wd) < 0) { + sway_log(SWAY_ERROR, "failed to restore working directory"); + } + free(wd); + + if (err_we != 0) { + free(config_dir); + return cmd_results_new(CMD_FAILURE, "Error expanding path %s (code %d)", argv[0], err_we); + } + + struct cmd_results *res = NULL; + + if (p.we_wordc == 0) { + res = cmd_results_new(CMD_FAILURE, "Path expanded to nothing: %s", argv[0]); + goto cleanup; + } else if (p.we_wordc > 1) { + res = cmd_results_new(CMD_FAILURE, "Path expanded to multiple files: %s", argv[0]); + goto cleanup; + } + + expanded_path = strdup(p.we_wordv[0]); + if (!expanded_path) { + res = cmd_results_new(CMD_FAILURE, "Failed to allocate memory"); + goto cleanup; + } + + if (config->reading && expanded_path[0] != '/' && config_dir) { + char *real_path = malloc(strlen(config_dir) + strlen(expanded_path) + 2); + if (real_path) { + sprintf(real_path, "%s/%s", config_dir, expanded_path); + free(expanded_path); + expanded_path = real_path; + } + } + +cleanup: + wordfree(&p); + free(config_dir); + + if (res) { + return res; + } + + int err = luaL_loadfile(config->lua.state, expanded_path); if (err != LUA_OK) { - return cmd_results_new(CMD_FAILURE, "Error %d loading lua script %s", err, argv[0]); + struct cmd_results *res = cmd_results_new(CMD_FAILURE, "Error %d loading lua script %s", err, expanded_path); + free(expanded_path); + return res; } // Search if there is already a state for this script - struct sway_lua_script *script = find_script(config->lua.scripts, argv[0]); + struct sway_lua_script *script = find_script(config->lua.scripts, expanded_path); if (!script) { script = malloc(sizeof(struct sway_lua_script)); - script->name = strdup(argv[0]); + script->name = strdup(expanded_path); lua_createtable(config->lua.state, 0, 0); script->state = luaL_ref(config->lua.state, LUA_REGISTRYINDEX); list_add(config->lua.scripts, script); @@ -41,11 +112,16 @@ struct cmd_results *cmd_lua(int argc, char **argv) { err = lua_pcall(config->lua.state, 2, LUA_MULTRET, 0); if (err != LUA_OK) { const char *str = luaL_checkstring(config->lua.state, -1); + struct cmd_results *res; if (str) { - return cmd_results_new(CMD_FAILURE, "Error %s executing lua script %s", str, argv[0]); + res = cmd_results_new(CMD_FAILURE, "Error %s executing lua script %s", str, expanded_path); + } else { + res = cmd_results_new(CMD_FAILURE, "Error %d executing lua script %s", err, expanded_path); } - return cmd_results_new(CMD_FAILURE, "Error %d executing lua script %s", err, argv[0]); + free(expanded_path); + return res; } + free(expanded_path); return cmd_results_new(CMD_SUCCESS, NULL); } diff --git a/sway/commands/swap.c b/sway/commands/swap.c index 622f94f8a..fec6cb7ee 100644 --- a/sway/commands/swap.c +++ b/sway/commands/swap.c @@ -8,15 +8,12 @@ #include "sway/tree/root.h" #include "sway/tree/view.h" #include "sway/tree/workspace.h" +#include "sway/tree/node.h" #include "stringop.h" static const char expected_syntax[] = "Expected 'swap container with id|con_id|mark '"; -static bool test_con_id(struct sway_container *container, void *data) { - size_t *con_id = data; - return container->node.id == *con_id; -} #if WLR_HAS_XWAYLAND static bool test_id(struct sway_container *container, void *data) { @@ -58,8 +55,16 @@ struct cmd_results *cmd_swap(int argc, char **argv) { other = root_find_container(test_id, &id); #endif } else if (strcasecmp(argv[2], "con_id") == 0) { - size_t con_id = atoi(value); - other = root_find_container(test_con_id, &con_id); + char *endptr; + size_t con_id = strtoul(value, &endptr, 10); + if (*value == '\0' || *endptr != '\0') { + free(value); + return cmd_results_new(CMD_INVALID, "Invalid container ID"); + } + struct sway_node *node = node_by_id(con_id); + if (node && node->type == N_CONTAINER) { + other = node->sway_container; + } } else if (strcasecmp(argv[2], "mark") == 0) { other = root_find_container(test_mark, value); } else { diff --git a/sway/config.c b/sway/config.c index 2295cef73..c7af5397b 100644 --- a/sway/config.c +++ b/sway/config.c @@ -193,6 +193,7 @@ void free_config(struct sway_config *config) { free(config->floating_scroll_left_cmd); free(config->floating_scroll_right_cmd); free(config->font); + pango_font_description_free(config->font_description); free(config->swaybg_command); free(config->swaynag_command); free((char *)config->current_config_path); diff --git a/sway/criteria.c b/sway/criteria.c index ed1140d17..ff673af99 100644 --- a/sway/criteria.c +++ b/sway/criteria.c @@ -11,6 +11,7 @@ #include "sway/tree/root.h" #include "sway/tree/view.h" #include "sway/tree/workspace.h" +#include "sway/tree/node.h" #include "stringop.h" #include "list.h" #include "log.h" @@ -527,6 +528,18 @@ static void criteria_get_containers_iterator(struct sway_container *container, list_t *criteria_get_containers(struct criteria *criteria) { list_t *matches = create_list(); + if (criteria->con_id) { + struct sway_node *node = node_by_id(criteria->con_id); + if (node && node->type == N_CONTAINER) { + struct sway_container *container = node->sway_container; + struct match_data data = { + .criteria = criteria, + .matches = matches, + }; + criteria_get_containers_iterator(container, &data); + } + return matches; + } struct match_data data = { .criteria = criteria, .matches = matches, diff --git a/sway/desktop/layer_shell.c b/sway/desktop/layer_shell.c index 9f687fa3d..23b70291d 100644 --- a/sway/desktop/layer_shell.c +++ b/sway/desktop/layer_shell.c @@ -259,6 +259,7 @@ static void handle_node_destroy(struct wl_listener *listener, void *data) { layer->layer_surface->data = NULL; wl_list_remove(&layer->link); + node_fini(&layer->node); free(layer); } @@ -339,6 +340,7 @@ static void popup_handle_destroy(struct wl_listener *listener, void *data) { wl_list_remove(&popup->new_popup.link); wl_list_remove(&popup->commit.link); wl_list_remove(&popup->reposition.link); + node_fini(&popup->node); free(popup); } diff --git a/sway/desktop/transaction.c b/sway/desktop/transaction.c index d4740ace6..bb5667b95 100644 --- a/sway/desktop/transaction.c +++ b/sway/desktop/transaction.c @@ -62,6 +62,20 @@ static void transaction_destroy(struct sway_transaction *transaction) { struct sway_transaction_instruction *instruction = transaction->instructions->items[i]; struct sway_node *node = instruction->node; + switch (node->type) { + case N_OUTPUT: + list_free(instruction->output_state.workspaces); + break; + case N_WORKSPACE: + list_free(instruction->workspace_state.floating); + list_free(instruction->workspace_state.tiling); + break; + case N_CONTAINER: + list_free(instruction->container_state.children); + break; + default: + break; + } node->ntxnrefs--; if (node->instruction == instruction) { node->instruction = NULL; @@ -242,6 +256,7 @@ static void apply_output_state(struct sway_output *output, struct sway_output_state *state) { list_free(output->current.workspaces); memcpy(&output->current, state, sizeof(struct sway_output_state)); + state->workspaces = NULL; } static void apply_workspace_state(struct sway_workspace *ws, @@ -249,6 +264,8 @@ static void apply_workspace_state(struct sway_workspace *ws, list_free(ws->current.floating); list_free(ws->current.tiling); memcpy(&ws->current, state, sizeof(struct sway_workspace_state)); + state->floating = NULL; + state->tiling = NULL; } static void apply_container_state(struct sway_container *container, @@ -262,6 +279,7 @@ static void apply_container_state(struct sway_container *container, list_free(container->current.children); memcpy(&container->current, state, sizeof(struct sway_container_state)); + state->children = NULL; if (view) { if (view->saved_surface_tree) { @@ -2190,3 +2208,43 @@ void transaction_commit_delayed(void) { transaction_commit_pending(); } + +void transaction_shut_down(struct sway_server *server) { + if (server->dirty_nodes) { + for (int i = 0; i < server->dirty_nodes->length; ++i) { + struct sway_node *node = server->dirty_nodes->items[i]; + node->dirty = false; + } + } + + if (server->queued_transaction) { + transaction_destroy(server->queued_transaction); + server->queued_transaction = NULL; + } + + if (server->pending_transaction) { + transaction_destroy(server->pending_transaction); + server->pending_transaction = NULL; + } + + if (server->dirty_nodes) { + for (int i = server->dirty_nodes->length - 1; i >= 0; --i) { + struct sway_node *node = server->dirty_nodes->items[i]; + if (node->destroying && node->ntxnrefs == 0) { + switch (node->type) { + case N_OUTPUT: + output_destroy(node->sway_output); + break; + case N_WORKSPACE: + workspace_destroy(node->sway_workspace); + break; + case N_CONTAINER: + container_destroy(node->sway_container); + break; + default: + break; + } + } + } + } +} diff --git a/sway/input/input-manager.c b/sway/input/input-manager.c index ffcf8fc52..bfb2c1b82 100644 --- a/sway/input/input-manager.c +++ b/sway/input/input-manager.c @@ -501,6 +501,17 @@ void input_manager_finish(struct sway_input_manager *input) { wl_list_remove(&input->transient_seat_create.link); } +void input_manager_destroy(struct sway_input_manager *input) { + if (!input) { + return; + } + struct sway_seat *seat, *next_seat; + wl_list_for_each_safe(seat, next_seat, &input->seats, link) { + seat_destroy(seat); + } + free(input); +} + bool input_manager_has_focus(struct sway_node *node) { struct sway_seat *seat = NULL; wl_list_for_each(seat, &server.input->seats, link) { diff --git a/sway/input/seat.c b/sway/input/seat.c index c92913d40..c50a67fd8 100644 --- a/sway/input/seat.c +++ b/sway/input/seat.c @@ -74,6 +74,8 @@ void seat_destroy(struct sway_seat *seat) { static void handle_seat_destroy(struct wl_listener *listener, void *data) { struct sway_seat *seat = wl_container_of(listener, seat, destroy); + seatop_end(seat); + if (seat == config->handler_context.seat) { config->handler_context.seat = input_manager_get_default_seat(); } diff --git a/sway/lua.c b/sway/lua.c index 545e83fc9..f888ec4ba 100644 --- a/sway/lua.c +++ b/sway/lua.c @@ -7,6 +7,7 @@ #include "sway/tree/view.h" #include "sway/tree/container.h" #include "sway/tree/workspace.h" +#include "sway/tree/node.h" #include "sway/output.h" #include "sway/ipc-server.h" @@ -72,6 +73,15 @@ static int scroll_log(lua_State *L) { return 0; } +static void safe_pcall(lua_State *L, int nargs) { + int err = lua_pcall(L, nargs, 0, 0); + if (err != LUA_OK) { + const char *msg = lua_tostring(L, -1); + sway_log(SWAY_ERROR, "Lua error: %s", msg ? msg : "unknown error"); + lua_pop(L, 1); + } +} + static int scroll_state_get_value(lua_State *L) { int argc = lua_gettop(L); if (argc < 2) { @@ -192,15 +202,6 @@ static int scroll_ipc_send(lua_State *L) { return 0; } -static bool find_container(struct sway_container *container, void *data) { - struct sway_container *con = data; - return container == con; -} - -static bool find_workspace(struct sway_workspace *workspace, void *data) { - struct sway_workspace *ws = data; - return workspace == ws; -} static int scroll_command_error(lua_State *L, const char *error) { lua_createtable(L, 1, 0); @@ -209,6 +210,62 @@ static int scroll_command_error(lua_State *L, const char *error) { return 1; } +static struct sway_node *lua_to_node(lua_State *L, int index) { + if (lua_isnil(L, index)) { + return NULL; + } + if (!lua_isinteger(L, index)) { + return NULL; + } + size_t id = lua_tointeger(L, index); + return node_by_id(id); +} + +static struct sway_container *lua_to_container(lua_State *L, int index) { + struct sway_node *node = lua_to_node(L, index); + return (node && node->type == N_CONTAINER) ? node->sway_container : NULL; +} + +static struct sway_workspace *lua_to_workspace(lua_State *L, int index) { + struct sway_node *node = lua_to_node(L, index); + return (node && node->type == N_WORKSPACE) ? node->sway_workspace : NULL; +} + +static struct sway_output *lua_to_output(lua_State *L, int index) { + struct sway_node *node = lua_to_node(L, index); + return (node && node->type == N_OUTPUT) ? node->sway_output : NULL; +} + +static struct sway_view *lua_to_view(lua_State *L, int index) { + struct sway_container *con = lua_to_container(L, index); + return con ? con->view : NULL; +} + +static void lua_push_node(lua_State *L, struct sway_node *node) { + if (node) { + lua_pushinteger(L, node->id); + } else { + lua_pushnil(L); + } +} + + + +static int scroll_node_get_type(lua_State *L) { + int argc = lua_gettop(L); + if (argc == 0) { + lua_pushnil(L); + return 1; + } + struct sway_node *node = lua_to_node(L, -1); + if (!node) { + lua_pushnil(L); + return 1; + } + lua_pushstring(L, node_type_to_str(node->type)); + return 1; +} + void lua_command_data_create() { luaL_unref(config->lua.state, LUA_REGISTRYINDEX, config->lua.command_data); config->lua.command_data = luaL_ref(config->lua.state, LUA_REGISTRYINDEX); @@ -220,32 +277,29 @@ static int scroll_command(lua_State *L) { if (argc < 2) { return scroll_command_error(L, "Error: scroll_command() received a wrong number of parameters"); } - void *node = lua_isnil(L, 1) ? NULL : lua_touserdata(L, 1); + struct sway_container *container = NULL; + struct sway_workspace *workspace = NULL; struct sway_seat *seat = input_manager_current_seat(); - struct sway_container *container = node; - if (container && container->node.type == N_CONTAINER) { - struct sway_container *found = root_find_container(find_container, container); - if (!found) { - return scroll_command_error(L, "Error: scroll_command() received a container parameter that does not exist"); - } - if (!container->view) { - return scroll_command_error(L, "Error: scroll_command() received a container parameter that does not have a view"); + + if (!lua_isnil(L, 1)) { + struct sway_node *node = lua_to_node(L, 1); + if (!node) { + return scroll_command_error(L, "Error: scroll_command() received a parameter that does not exist or is invalid"); } - seat = NULL; - } else { - container = NULL; - struct sway_workspace *workspace = node; - if (workspace && workspace->node.type == N_WORKSPACE) { - struct sway_workspace *found = root_find_workspace(find_workspace, workspace); - if (!found) { - return scroll_command_error(L, "Error: scroll_command() received a workspace parameter that does not exist"); + if (node->type == N_CONTAINER) { + container = node->sway_container; + if (!container->view) { + return scroll_command_error(L, "Error: scroll_command() received a container parameter that does not have a view"); } - seat_set_raw_focus(seat, &workspace->node); - } else if (node == NULL) { seat = NULL; + } else if (node->type == N_WORKSPACE) { + workspace = node->sway_workspace; + seat_set_raw_focus(seat, &workspace->node); } else { return scroll_command_error(L, "Error: scroll_command() received a parameter that is neither a container nor a workspace"); } + } else { + seat = NULL; } // Remove command_data luaL_unref(config->lua.state, LUA_REGISTRYINDEX, config->lua.command_data); @@ -272,7 +326,7 @@ static int scroll_command(lua_State *L) { lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_function); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, config->lua.command_data); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_data); - lua_call(config->lua.state, 2, 0); + safe_pcall(config->lua.state, 2); } return 1; } @@ -285,58 +339,31 @@ static struct sway_node *get_focused_node() { static int scroll_focused_view(lua_State *L) { struct sway_node *node = get_focused_node(); - if (!node) { - lua_pushnil(L); - return 1; - } - struct sway_container *container = node->type == N_CONTAINER ? + struct sway_container *container = (node && node->type == N_CONTAINER) ? node->sway_container : NULL; - - if (container && container->view) { - lua_pushlightuserdata(L, container->view); - } else { - lua_pushnil(L); - } + lua_push_node(L, (container && container->view) ? &container->node : NULL); return 1; } static int scroll_focused_container(lua_State *L) { struct sway_node *node = get_focused_node(); - if (!node) { - lua_pushnil(L); - return 1; - } - struct sway_container *container = node->type == N_CONTAINER ? + struct sway_container *container = (node && node->type == N_CONTAINER) ? node->sway_container : NULL; - - if (container) { - lua_pushlightuserdata(L, container); - } else { - lua_pushnil(L); - } + lua_push_node(L, (struct sway_node *)container); return 1; } static int scroll_focused_workspace(lua_State *L) { struct sway_node *node = get_focused_node(); - if (!node) { - lua_pushnil(L); - return 1; - } - struct sway_workspace *workspace; - if (node->type == N_WORKSPACE) { - workspace = node->sway_workspace; - } else if (node->type == N_CONTAINER) { - workspace = node->sway_container->pending.workspace; - } else { - workspace = NULL; - } - - if (workspace) { - lua_pushlightuserdata(L, workspace); - } else { - lua_pushnil(L); + struct sway_workspace *workspace = NULL; + if (node) { + if (node->type == N_WORKSPACE) { + workspace = node->sway_workspace; + } else if (node->type == N_CONTAINER) { + workspace = node->sway_container->pending.workspace; + } } + lua_push_node(L, (struct sway_node *)workspace); return 1; } @@ -346,11 +373,7 @@ static bool find_urgent(struct sway_container *container, void *data) { static int scroll_urgent_view(lua_State *L) { struct sway_container *container = root_find_container(find_urgent, NULL); - if (container && container->view) { - lua_pushlightuserdata(L, container->view); - } else { - lua_pushnil(L); - } + lua_push_node(L, (container && container->view) ? &container->node : NULL); return 1; } @@ -360,14 +383,8 @@ static int scroll_view_mapped(lua_State *L) { lua_pushboolean(L, 0); return 1; } - struct sway_view *view = lua_touserdata(L, -1); - if (view) { - if (view->lua.mapped) { - lua_pushboolean(L, 1); - return 1; - } - } - lua_pushboolean(L, 0); + struct sway_view *view = lua_to_view(L, -1); + lua_pushboolean(L, (view && view->lua.mapped) ? 1 : 0); return 1; } @@ -377,12 +394,8 @@ static int scroll_view_get_container(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_view *view = lua_touserdata(L, -1); - if (view && view->container) { - lua_pushlightuserdata(L,view->container); - } else { - lua_pushnil(L); - } + struct sway_container *con = lua_to_container(L, -1); + lua_push_node(L, (struct sway_node *)con); return 1; } @@ -392,7 +405,7 @@ static int scroll_view_get_app_id(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_view *view = lua_touserdata(L, -1); + struct sway_view *view = lua_to_view(L, -1); if (!view) { lua_pushnil(L); return 1; @@ -408,7 +421,7 @@ static int scroll_view_get_class(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_view *view = lua_touserdata(L, -1); + struct sway_view *view = lua_to_view(L, -1); if (!view) { lua_pushnil(L); return 1; @@ -424,7 +437,7 @@ static int scroll_view_get_title(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_view *view = lua_touserdata(L, -1); + struct sway_view *view = lua_to_view(L, -1); if (!view) { lua_pushnil(L); return 1; @@ -440,7 +453,7 @@ static int scroll_view_get_pid(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_view *view = lua_touserdata(L, -1); + struct sway_view *view = lua_to_view(L, -1); if (!view) { lua_pushnil(L); return 1; @@ -472,7 +485,7 @@ static int scroll_view_get_parent_view(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_view *view = lua_touserdata(L, -1); + struct sway_view *view = lua_to_view(L, -1); if (!view) { lua_pushnil(L); return 1; @@ -490,11 +503,7 @@ static int scroll_view_get_parent_view(lua_State *L) { break; } }; - if (container && container->view) { - lua_pushlightuserdata(L, container->view); - } else { - lua_pushnil(L); - } + lua_push_node(L, (container && container->view) ? &container->node : NULL); return 1; } @@ -504,7 +513,7 @@ static int scroll_view_get_urgent(lua_State *L) { lua_pushboolean(L, 0); return 1; } - struct sway_view *view = lua_touserdata(L, -1); + struct sway_view *view = lua_to_view(L, -1); if (!view) { lua_pushnil(L); return 1; @@ -519,7 +528,7 @@ static int scroll_view_set_urgent(lua_State *L) { return 0; } bool urgent = lua_toboolean(L, 2); - struct sway_view *view = lua_touserdata(L, 1); + struct sway_view *view = lua_to_view(L, 1); if (view) { view_set_urgent(view, urgent); } @@ -532,7 +541,7 @@ static int scroll_view_get_shell(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_view *view = lua_touserdata(L, -1); + struct sway_view *view = lua_to_view(L, -1); if (!view) { lua_pushnil(L); return 1; @@ -548,7 +557,7 @@ static int scroll_view_get_tag(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_view *view = lua_touserdata(L, -1); + struct sway_view *view = lua_to_view(L, -1); if (!view) { lua_pushnil(L); return 1; @@ -567,7 +576,7 @@ static int scroll_view_close(lua_State *L) { if (argc == 0) { return 0; } - struct sway_view *view = lua_touserdata(L, -1); + struct sway_view *view = lua_to_view(L, -1); if (view) { view_close(view); } @@ -579,8 +588,8 @@ static int scroll_container_set_focus(lua_State *L) { if (argc == 0) { return 0; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { return 0; } struct sway_seat *seat = input_manager_current_seat(); @@ -594,16 +603,9 @@ static int scroll_container_get_workspace(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { - lua_pushnil(L); - return 1; - } - if (container->pending.workspace) { - lua_pushlightuserdata(L, container->pending.workspace); - } else { - lua_pushnil(L); - } + struct sway_container *container = lua_to_container(L, -1); + lua_push_node(L, (container && container->pending.workspace) ? + (struct sway_node *)container->pending.workspace : NULL); return 1; } @@ -613,8 +615,8 @@ static int scroll_container_get_marks(lua_State *L) { lua_createtable(L, 0, 0); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_createtable(L, 0, 0); return 1; } @@ -634,8 +636,8 @@ static int scroll_container_get_floating(lua_State *L) { lua_pushboolean(L, 0); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushboolean(L, 0); return 1; } @@ -649,8 +651,8 @@ static int scroll_container_get_opacity(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushnil(L); return 1; } @@ -664,8 +666,8 @@ static int scroll_container_get_sticky(lua_State *L) { lua_pushboolean(L, 0); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushboolean(L, 0); return 1; } @@ -679,8 +681,8 @@ static int scroll_container_get_scratchpad(lua_State *L) { lua_pushboolean(L, 0); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushboolean(L, 0); return 1; } @@ -694,8 +696,8 @@ static int scroll_container_get_width_fraction(lua_State *L) { lua_pushnumber(L, 0.0); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushnumber(L, 0.0); return 1; } @@ -709,8 +711,8 @@ static int scroll_container_get_height_fraction(lua_State *L) { lua_pushnumber(L, 0.0); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushnumber(L, 0.0); return 1; } @@ -724,8 +726,8 @@ static int scroll_container_get_width(lua_State *L) { lua_pushnumber(L, 0.0); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushnumber(L, 0.0); return 1; } @@ -739,8 +741,8 @@ static int scroll_container_get_height(lua_State *L) { lua_pushnumber(L, 0.0); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushnumber(L, 0.0); return 1; } @@ -754,8 +756,8 @@ static int scroll_container_get_fullscreen_mode(lua_State *L) { lua_pushstring(L, "none"); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushstring(L, "none"); return 1; } @@ -779,8 +781,8 @@ static int scroll_container_get_fullscreen_app_mode(lua_State *L) { lua_pushstring(L, "disabled"); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushstring(L, "disabled"); return 1; } @@ -801,8 +803,8 @@ static int scroll_container_get_fullscreen_view_mode(lua_State *L) { lua_pushstring(L, "disabled"); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushstring(L, "disabled"); return 1; } @@ -823,8 +825,8 @@ static int scroll_container_get_fullscreen_layout_mode(lua_State *L) { lua_pushstring(L, "disabled"); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_pushstring(L, "disabled"); return 1; } @@ -845,8 +847,8 @@ static int scroll_container_get_pin_mode(lua_State *L) { lua_pushstring(L, "none"); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER || !container->pending.workspace) { + struct sway_container *container = lua_to_container(L, -1); + if (!container || !container->pending.workspace) { lua_pushstring(L, "none"); return 1; } @@ -872,13 +874,9 @@ static int scroll_container_get_parent(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER || - container->pending.parent == NULL) { - lua_pushnil(L); - return 1; - } - lua_pushlightuserdata(L, container->pending.parent); + struct sway_container *container = lua_to_container(L, -1); + lua_push_node(L, (container && container->pending.parent) ? + (struct sway_node *)container->pending.parent : NULL); return 1; } @@ -888,9 +886,8 @@ static int scroll_container_get_children(lua_State *L) { lua_createtable(L, 0, 0); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER || - container->pending.children == NULL) { + struct sway_container *container = lua_to_container(L, -1); + if (!container || container->pending.children == NULL) { lua_createtable(L, 0, 0); return 1; } @@ -899,7 +896,7 @@ static int scroll_container_get_children(lua_State *L) { lua_createtable(L, len, 0); for (int i = 0; i < len; ++i) { struct sway_container *con = container->pending.children->items[i]; - lua_pushlightuserdata(L, con); + lua_push_node(L, (struct sway_node *)con); lua_rawseti(L, -2, i + 1); } return 1; @@ -911,14 +908,14 @@ static int scroll_container_get_views(lua_State *L) { lua_createtable(L, 0, 0); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { + struct sway_container *container = lua_to_container(L, -1); + if (!container) { lua_createtable(L, 0, 0); return 1; } if (container->view) { lua_createtable(L, 1, 0); - lua_pushlightuserdata(L, container->view); + lua_push_node(L, (struct sway_node *)container); lua_rawseti(L, -2, 1); } else { int len = container->pending.children->length; @@ -926,7 +923,7 @@ static int scroll_container_get_views(lua_State *L) { lua_createtable(L, len, 0); for (int i = 0; i < len; ++i) { struct sway_container *con = container->pending.children->items[i]; - lua_pushlightuserdata(L, con->view); + lua_push_node(L, con->view ? (struct sway_node *)con : NULL); lua_rawseti(L, -2, i + 1); } } @@ -939,22 +936,19 @@ static int scroll_container_get_id(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER) { - lua_pushnil(L); - return 1; - } - lua_pushinteger(L, container->node.id); + struct sway_container *container = lua_to_container(L, -1); + lua_push_node(L, (struct sway_node *)container); return 1; } + static int scroll_workspace_set_focus(lua_State *L) { int argc = lua_gettop(L); if (argc == 0) { return 0; } - struct sway_workspace *workspace = lua_touserdata(L, -1); - if (!workspace || workspace->node.type != N_WORKSPACE) { + struct sway_workspace *workspace = lua_to_workspace(L, -1); + if (!workspace) { return 0; } struct sway_seat *seat = input_manager_current_seat(); @@ -972,8 +966,8 @@ static int scroll_workspace_get_name(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_workspace *workspace = lua_touserdata(L, -1); - if (!workspace || workspace->node.type != N_WORKSPACE) { + struct sway_workspace *workspace = lua_to_workspace(L, -1); + if (!workspace) { lua_pushnil(L); return 1; } @@ -987,9 +981,8 @@ static int scroll_workspace_get_tiling(lua_State *L) { lua_createtable(L, 0, 0); return 1; } - struct sway_workspace *workspace = lua_touserdata(L, -1); - if (!workspace || workspace->node.type != N_WORKSPACE || - workspace->tiling->length == 0) { + struct sway_workspace *workspace = lua_to_workspace(L, -1); + if (!workspace || workspace->tiling->length == 0) { lua_createtable(L, 0, 0); return 1; } @@ -997,7 +990,7 @@ static int scroll_workspace_get_tiling(lua_State *L) { lua_createtable(L, workspace->tiling->length, 0); for (int i = 0; i < workspace->tiling->length; ++i) { struct sway_container *container = workspace->tiling->items[i]; - lua_pushlightuserdata(L, container); + lua_push_node(L, (struct sway_node *)container); lua_rawseti(L, -2, i + 1); } return 1; @@ -1009,9 +1002,8 @@ static int scroll_workspace_get_floating(lua_State *L) { lua_createtable(L, 0, 0); return 1; } - struct sway_workspace *workspace = lua_touserdata(L, -1); - if (!workspace || workspace->node.type != N_WORKSPACE || - workspace->floating->length == 0) { + struct sway_workspace *workspace = lua_to_workspace(L, -1); + if (!workspace || workspace->floating->length == 0) { lua_createtable(L, 0, 0); return 1; } @@ -1019,7 +1011,7 @@ static int scroll_workspace_get_floating(lua_State *L) { lua_createtable(L, workspace->floating->length, 0); for (int i = 0; i < workspace->floating->length; ++i) { struct sway_container *container = workspace->floating->items[i]; - lua_pushlightuserdata(L, container); + lua_push_node(L, (struct sway_node *)container); lua_rawseti(L, -2, i + 1); } return 1; @@ -1031,8 +1023,8 @@ static int scroll_workspace_get_mode(lua_State *L) { lua_createtable(L, 0, 0); return 1; } - struct sway_workspace *workspace = lua_touserdata(L, -1); - if (!workspace || workspace->node.type != N_WORKSPACE) { + struct sway_workspace *workspace = lua_to_workspace(L, -1); + if (!workspace) { lua_createtable(L, 0, 0); return 1; } @@ -1096,8 +1088,8 @@ static int scroll_workspace_set_mode(lua_State *L) { if (argc < 2) { return 0; } - struct sway_workspace *workspace = lua_touserdata(L, 1); - if (!workspace || workspace->node.type != N_WORKSPACE) { + struct sway_workspace *workspace = lua_to_workspace(L, 1); + if (!workspace) { return 0; } if (lua_getfield(L, 2, "mode") == LUA_TSTRING) { @@ -1158,8 +1150,8 @@ static int scroll_workspace_get_layout_type(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_workspace *workspace = lua_touserdata(L, -1); - if (!workspace || workspace->node.type != N_WORKSPACE) { + struct sway_workspace *workspace = lua_to_workspace(L, -1); + if (!workspace) { lua_pushnil(L); return 1; } @@ -1183,8 +1175,8 @@ static int scroll_workspace_set_layout_type(lua_State *L) { if (argc < 2) { return 0; } - struct sway_workspace *workspace = lua_touserdata(L, 1); - if (!workspace || workspace->node.type != N_WORKSPACE) { + struct sway_workspace *workspace = lua_to_workspace(L, 1); + if (!workspace) { return 0; } const char *layout = luaL_checkstring(L, 2); @@ -1202,8 +1194,8 @@ static int scroll_workspace_get_width(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_workspace *workspace = lua_touserdata(L, -1); - if (!workspace || workspace->node.type != N_WORKSPACE) { + struct sway_workspace *workspace = lua_to_workspace(L, -1); + if (!workspace) { lua_pushnil(L); return 1; } @@ -1217,8 +1209,8 @@ static int scroll_workspace_get_height(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_workspace *workspace = lua_touserdata(L, -1); - if (!workspace || workspace->node.type != N_WORKSPACE) { + struct sway_workspace *workspace = lua_to_workspace(L, -1); + if (!workspace) { lua_pushnil(L); return 1; } @@ -1232,16 +1224,8 @@ static int scroll_workspace_get_output(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_workspace *workspace = lua_touserdata(L, -1); - if (!workspace || workspace->node.type != N_WORKSPACE) { - lua_pushnil(L); - return 1; - } - if (workspace->output) { - lua_pushlightuserdata(L, workspace->output); - } else { - lua_pushnil(L); - } + struct sway_workspace *workspace = lua_to_workspace(L, -1); + lua_push_node(L, (workspace && workspace->output) ? (struct sway_node *)workspace->output : NULL); return 1; } @@ -1251,13 +1235,9 @@ static int scroll_workspace_get_pin(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_workspace *workspace = lua_touserdata(L, -1); - if (!workspace || workspace->node.type != N_WORKSPACE || - !workspace->layout.pin.container) { - lua_pushnil(L); - return 1; - } - lua_pushlightuserdata(L, workspace->layout.pin.container); + struct sway_workspace *workspace = lua_to_workspace(L, -1); + lua_push_node(L, (workspace && workspace->layout.pin.container) ? + (struct sway_node *)workspace->layout.pin.container : NULL); return 1; } @@ -1267,8 +1247,8 @@ static int scroll_workspace_get_split(lua_State *L) { lua_createtable(L, 0, 0); return 1; } - struct sway_workspace *workspace = lua_touserdata(L, -1); - if (!workspace || workspace->node.type != N_WORKSPACE) { + struct sway_workspace *workspace = lua_to_workspace(L, -1); + if (!workspace) { lua_createtable(L, 0, 0); return 1; } @@ -1301,7 +1281,7 @@ static int scroll_workspace_get_split(lua_State *L) { lua_pushinteger(L, workspace->split.gap); lua_setfield(L, -2, "gap"); - lua_pushlightuserdata(L, workspace->split.sibling); + lua_push_node(L, workspace->split.sibling ? (struct sway_node *)workspace->split.sibling : NULL); lua_setfield(L, -2, "sibling"); return 1; @@ -1312,7 +1292,7 @@ static int scroll_scratchpad_get_containers(lua_State *L) { lua_createtable(L, root->scratchpad->length, 0); for (int i = 0; i < root->scratchpad->length; ++i) { struct sway_container *container = root->scratchpad->items[i]; - lua_pushlightuserdata(L, container); + lua_push_node(L, (struct sway_node *)container); lua_rawseti(L, -2, i + 1); } return 1; @@ -1323,8 +1303,8 @@ static int scroll_scratchpad_show(lua_State *L) { if (argc == 0) { return 0; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER || !container->scratchpad) { + struct sway_container *container = lua_to_container(L, -1); + if (!container || !container->scratchpad) { return 0; } root_scratchpad_show(container); @@ -1336,8 +1316,8 @@ static int scroll_scratchpad_hide(lua_State *L) { if (argc == 0) { return 0; } - struct sway_container *container = lua_touserdata(L, -1); - if (!container || container->node.type != N_CONTAINER || !container->scratchpad) { + struct sway_container *container = lua_to_container(L, -1); + if (!container || !container->scratchpad) { return 0; } root_scratchpad_hide(container); @@ -1350,16 +1330,9 @@ static int scroll_output_get_active_workspace(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_output *output = lua_touserdata(L, -1); - if (!output || output->node.type != N_OUTPUT) { - lua_pushnil(L); - return 1; - } - if (output->current.active_workspace) { - lua_pushlightuserdata(L, output->current.active_workspace); - } else { - lua_pushnil(L); - } + struct sway_output *output = lua_to_output(L, -1); + lua_push_node(L, (output && output->current.active_workspace) ? + (struct sway_node *)output->current.active_workspace : NULL); return 1; } @@ -1369,8 +1342,8 @@ static int scroll_output_get_enabled(lua_State *L) { lua_pushboolean(L, 0); return 1; } - struct sway_output *output = lua_touserdata(L, -1); - if (!output || output->node.type != N_OUTPUT) { + struct sway_output *output = lua_to_output(L, -1); + if (!output) { lua_pushboolean(L, 0); return 1; } @@ -1384,8 +1357,8 @@ static int scroll_output_get_name(lua_State *L) { lua_pushnil(L); return 1; } - struct sway_output *output = lua_touserdata(L, -1); - if (!output || output->node.type != N_OUTPUT) { + struct sway_output *output = lua_to_output(L, -1); + if (!output) { lua_pushnil(L); return 1; } @@ -1399,8 +1372,8 @@ static int scroll_output_get_workspaces(lua_State *L) { lua_createtable(L, 0, 0); return 1; } - struct sway_output *output = lua_touserdata(L, -1); - if (!output || output->node.type != N_OUTPUT) { + struct sway_output *output = lua_to_output(L, -1); + if (!output) { lua_createtable(L, 0, 0); return 1; } @@ -1408,7 +1381,7 @@ static int scroll_output_get_workspaces(lua_State *L) { lua_createtable(L, output->workspaces->length, 0); for (int i = 0; i < output->workspaces->length; ++i) { struct sway_workspace *workspace = output->workspaces->items[i]; - lua_pushlightuserdata(L, workspace); + lua_push_node(L, (struct sway_node *)workspace); lua_rawseti(L, -2, i + 1); } return 1; @@ -1419,7 +1392,7 @@ static int scroll_root_get_outputs(lua_State *L) { lua_createtable(L, root->outputs->length, 0); for (int i = 0; i < root->outputs->length; ++i) { struct sway_output *output = root->outputs->items[i]; - lua_pushlightuserdata(L, output); + lua_push_node(L, (struct sway_node *)output); lua_rawseti(L, -2, i + 1); } return 1; @@ -1583,6 +1556,7 @@ static luaL_Reg const scroll_lib[] = { { "state_get_value", scroll_state_get_value }, { "ipc_send", scroll_ipc_send }, { "command", scroll_command }, + { "node_get_type", scroll_node_get_type }, { "focused_view", scroll_focused_view }, { "focused_container", scroll_focused_container }, { "focused_workspace", scroll_focused_workspace }, @@ -1655,9 +1629,9 @@ void lua_execute_view_map_cbs(struct sway_view *view) { for (int i = 0; i < config->lua.cbs_view_map->length; ++i) { struct sway_lua_closure *closure = config->lua.cbs_view_map->items[i]; lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_function); - lua_pushlightuserdata(config->lua.state, view); + lua_push_node(config->lua.state, (view && view->container) ? (struct sway_node *)view->container : NULL); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_data); - lua_call(config->lua.state, 2, 0); + safe_pcall(config->lua.state, 2); } } @@ -1665,9 +1639,9 @@ void lua_execute_view_unmap_cbs(struct sway_view *view) { for (int i = 0; i < config->lua.cbs_view_unmap->length; ++i) { struct sway_lua_closure *closure = config->lua.cbs_view_unmap->items[i]; lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_function); - lua_pushlightuserdata(config->lua.state, view); + lua_push_node(config->lua.state, (view && view->container) ? (struct sway_node *)view->container : NULL); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_data); - lua_call(config->lua.state, 2, 0); + safe_pcall(config->lua.state, 2); } } @@ -1675,9 +1649,9 @@ void lua_execute_view_urgent_cbs(struct sway_view *view) { for (int i = 0; i < config->lua.cbs_view_urgent->length; ++i) { struct sway_lua_closure *closure = config->lua.cbs_view_urgent->items[i]; lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_function); - lua_pushlightuserdata(config->lua.state, view); + lua_push_node(config->lua.state, (view && view->container) ? (struct sway_node *)view->container : NULL); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_data); - lua_call(config->lua.state, 2, 0); + safe_pcall(config->lua.state, 2); } } @@ -1686,9 +1660,9 @@ void lua_execute_view_focus_cbs(struct sway_view *view) { for (int i = 0; i < config->lua.cbs_view_focus->length; ++i) { struct sway_lua_closure *closure = config->lua.cbs_view_focus->items[i]; lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_function); - lua_pushlightuserdata(config->lua.state, view); + lua_push_node(config->lua.state, (view && view->container) ? (struct sway_node *)view->container : NULL); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_data); - lua_call(config->lua.state, 2, 0); + safe_pcall(config->lua.state, 2); } } @@ -1696,9 +1670,9 @@ void lua_execute_view_float_cbs(struct sway_view *view) { for (int i = 0; i < config->lua.cbs_view_float->length; ++i) { struct sway_lua_closure *closure = config->lua.cbs_view_float->items[i]; lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_function); - lua_pushlightuserdata(config->lua.state, view); + lua_push_node(config->lua.state, (view && view->container) ? (struct sway_node *)view->container : NULL); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_data); - lua_call(config->lua.state, 2, 0); + safe_pcall(config->lua.state, 2); } } @@ -1706,9 +1680,9 @@ void lua_execute_workspace_create_cbs(struct sway_workspace *workspace) { for (int i = 0; i < config->lua.cbs_workspace_create->length; ++i) { struct sway_lua_closure *closure = config->lua.cbs_workspace_create->items[i]; lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_function); - lua_pushlightuserdata(config->lua.state, workspace); + lua_push_node(config->lua.state, (struct sway_node *)workspace); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_data); - lua_call(config->lua.state, 2, 0); + safe_pcall(config->lua.state, 2); } } @@ -1716,9 +1690,9 @@ void lua_execute_workspace_focus_cbs(struct sway_workspace *workspace) { for (int i = 0; i < config->lua.cbs_workspace_focus->length; ++i) { struct sway_lua_closure *closure = config->lua.cbs_workspace_focus->items[i]; lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_function); - lua_pushlightuserdata(config->lua.state, workspace); + lua_push_node(config->lua.state, (struct sway_node *)workspace); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_data); - lua_call(config->lua.state, 2, 0); + safe_pcall(config->lua.state, 2); } } @@ -1727,10 +1701,10 @@ void lua_execute_ipc_view_cbs(struct sway_view *view, const char *change) { for (int i = 0; i < config->lua.cbs_ipc_view->length; ++i) { struct sway_lua_closure *closure = config->lua.cbs_ipc_view->items[i]; lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_function); - lua_pushlightuserdata(config->lua.state, view); + lua_push_node(config->lua.state, view->container ? (struct sway_node *)view->container : NULL); lua_pushstring(config->lua.state, change); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_data); - lua_call(config->lua.state, 3, 0); + safe_pcall(config->lua.state, 3); } } } @@ -1740,11 +1714,11 @@ void lua_execute_ipc_workspace_cbs(struct sway_workspace *old_ws, for (int i = 0; i < config->lua.cbs_ipc_workspace->length; ++i) { struct sway_lua_closure *closure = config->lua.cbs_ipc_workspace->items[i]; lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_function); - lua_pushlightuserdata(config->lua.state, old_ws); - lua_pushlightuserdata(config->lua.state, new_ws); + lua_push_node(config->lua.state, (struct sway_node *)old_ws); + lua_push_node(config->lua.state, (struct sway_node *)new_ws); lua_pushstring(config->lua.state, change); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_data); - lua_call(config->lua.state, 4, 0); + safe_pcall(config->lua.state, 4); } } @@ -1752,12 +1726,8 @@ void lua_execute_jump_end_cbs(struct sway_container *container) { for (int i = 0; i < config->lua.cbs_jump_end->length; ++i) { struct sway_lua_closure *closure = config->lua.cbs_jump_end->items[i]; lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_function); - if (container) { - lua_pushlightuserdata(config->lua.state, container->view); - } else { - lua_pushnil(config->lua.state); - } + lua_push_node(config->lua.state, (struct sway_node *)container); lua_rawgeti(config->lua.state, LUA_REGISTRYINDEX, closure->cb_data); - lua_call(config->lua.state, 2, 0); + safe_pcall(config->lua.state, 2); } } diff --git a/sway/main.c b/sway/main.c index 9715676c2..1c98152e0 100644 --- a/sway/main.c +++ b/sway/main.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,7 @@ #include "sway/desktop/transaction.h" #include "sway/desktop/animation.h" #include "sway/tree/root.h" +#include "sway/tree/node.h" #include "sway/ipc-server.h" #include "ipc-client.h" #include "log.h" @@ -33,6 +35,8 @@ static bool terminate_request = false; static int exit_value = 0; static struct rlimit original_nofile_rlimit = {0}; +static struct wl_event_source *sigterm_source = NULL; +static struct wl_event_source *sigint_source = NULL; struct sway_server server = {0}; struct sway_debug debug = {0}; @@ -180,8 +184,8 @@ void handler(int sig) { #endif static void init_signals(void) { - wl_event_loop_add_signal(server.wl_event_loop, SIGTERM, term_signal, NULL); - wl_event_loop_add_signal(server.wl_event_loop, SIGINT, term_signal, NULL); + sigterm_source = wl_event_loop_add_signal(server.wl_event_loop, SIGTERM, term_signal, NULL); + sigint_source = wl_event_loop_add_signal(server.wl_event_loop, SIGINT, term_signal, NULL); struct sigaction sa_ign = { .sa_handler = SIG_IGN }; // avoid need to reap children @@ -428,9 +432,17 @@ int main(int argc, char **argv) { shutdown: sway_log(SWAY_INFO, "Shutting down scroll"); + if (sigterm_source) { + wl_event_source_remove(sigterm_source); + } + if (sigint_source) { + wl_event_source_remove(sigint_source); + } + server_fini(&server); root_destroy(root); root = NULL; + node_map_fini(); animation_destroy(); free(config_path); @@ -441,6 +453,8 @@ int main(int argc, char **argv) { } pango_cairo_font_map_set_default(NULL); + cairo_debug_reset_static_data(); + FcFini(); return exit_value; } diff --git a/sway/meson.build b/sway/meson.build index 37f49f541..3ce53f005 100644 --- a/sway/meson.build +++ b/sway/meson.build @@ -250,6 +250,7 @@ sway_sources = files( sway_deps = [ cairo, drm, + fontconfig, jsonc, libevdev, libinput, diff --git a/sway/scroll.5.scd b/sway/scroll.5.scd index 7537c695c..eb1faf46b 100644 --- a/sway/scroll.5.scd +++ b/sway/scroll.5.scd @@ -1802,9 +1802,13 @@ lua $lua_scripts/workspace_exec.lua 1 exec kitty windows. See *focus_on_window_activation* to set the state of the activated window to _urgent_ or _focused_. -*lua* script - Loads and executes the lua script. See *LUA* for details about the API and - some examples. +*lua* [] + Loads and executes the lua script at _path_ with optional _args_. _path_ + expands shell syntax (see *wordexp*(3) for details) and must resolve to + exactly one file. When invoked during configuration file loading, relative + paths are resolved against the directory of the configuration file. + Otherwise, they are resolved against the initial working directory of the + compositor. See *LUA* for details about the API and some examples. # CRITERIA @@ -2008,15 +2012,18 @@ print(val) Each time you run the script, it will increment and print the value for "test". -When writing Lua scripts, you can access pointers to scroll's workspaces, -containers or views. A view (window) has an immutable pointer throughout its +When writing Lua scripts, you can access IDs of scroll's workspaces, +containers or views. A view (window) has an immutable ID throughout its lifetime, from creation (mapping) to destruction (unmapping). A container, instead, can be destroyed or transformed when moving, changing workspace or in -several other ciscumstances. So if you want to store some references in the -state from one run of your script to another, store view pointers, never +several other circumstances. So if you want to store some references in the +state from one run of your script to another, store view IDs, never containers. You can always get the valid enclosing container for a view using the API when you need it. +Using an ID of a destroyed workspace or container will not crash the compositor; +the API functions will safely return nil or default values. + This is the current supported API: *log(message)* @@ -2042,23 +2049,28 @@ This is the current supported API: scroll.command(nil, "set_size v 0.33333333; move left nomode") ``` +*node_get_type(node)* + Returns the type of the given _node_ as a string. Possible values are: + "root", "output", "workspace", "container", "layer_surface", "layer_popup". + Returns nil if the node ID is invalid or not found. + *focused_view()* - Returns the focused view pointer or nil if none + Returns the focused view ID or nil if none *focused_container()* - Returns the focused container pointer or nil if none + Returns the focused container ID or nil if none *focused_workspace()* - Returns the focused workspace pointer or nil if none + Returns the focused workspace ID or nil if none *urgent_view()* - Returns the current urgent view pointer or nil if none. + Returns the current urgent view ID or nil if none. *view_mapped(view)* Returns _true_ if the _view_ is mapped (exists), otherwise returns _false_. *view_get_container(view)* - Returns the container pointer associated to _view_. + Returns the container ID associated to _view_. *view_get_app_id(view)* Returns the _app_id_ string for _view_, or nil if any error happens. @@ -2099,7 +2111,7 @@ scroll.command(nil, "set_size v 0.33333333; move left nomode") Sets the focus on _container_. *container_get_workspace(container)* - Returns the _container's_ parent workspace pointer, or nil if none. + Returns the _container's_ parent workspace ID, or nil if none. *container_get_marks(container)* Returns an array with all the marks associated to _container_. @@ -2232,7 +2244,7 @@ scroll.command(nil, "set_size v 0.33333333; move left nomode") Returns an integer number with the _workspace_'s height in pixels. *workspace_get_pin(workspace)* - Returns the _workspace's_ pinned container pointer, or nil if none. + Returns the _workspace's_ pinned container ID, or nil if none. *workspace_get_split(workspace)* Returns a table with the split state for _workspace_. The @@ -2244,10 +2256,10 @@ scroll.command(nil, "set_size v 0.33333333; move left nomode") _gap_: integer number - _sibling_: the sibling workspace's pointer + _sibling_: the sibling workspace's ID *workspace_get_output(workspace)* - Returns the _workspace's_ output pointer, or nil if none. + Returns the _workspace's_ output ID, or nil if none. *output_get_enabled(output)* Returns _true_ if the output (display) is enabled, or _false_ otherwise. diff --git a/sway/server.c b/sway/server.c index ba953d70b..b8750df1d 100644 --- a/sway/server.c +++ b/sway/server.c @@ -63,7 +63,9 @@ #include "sway/input/cursor.h" #include "sway/tree/root.h" #include "sway/tree/workspace.h" +#include "sway/tree/node.h" #include "sway/desktop/animation.h" +#include "sway/desktop/transaction.h" #if WLR_HAS_XWAYLAND #include @@ -244,6 +246,7 @@ static void handle_new_foreign_toplevel_capture_request(struct wl_listener *list } bool server_init(struct sway_server *server) { + node_map_init(); sway_log(SWAY_DEBUG, "Initializing Wayland server"); server->wl_display = wl_display_create(); if (!server->wl_display) { @@ -752,8 +755,16 @@ void server_fini(struct sway_server *server) { #endif wl_display_destroy_clients(server->wl_display); wlr_backend_destroy(server->backend); + transaction_shut_down(server); + input_manager_destroy(server->input); + server->input = NULL; + wlr_allocator_destroy(server->allocator); + server->allocator = NULL; + wlr_renderer_destroy(server->renderer); + server->renderer = NULL; wl_display_destroy(server->wl_display); list_free(server->dirty_nodes); + server->dirty_nodes = NULL; free(server->socket); } diff --git a/sway/tree/container.c b/sway/tree/container.c index a3e7b1e70..404e62b9e 100644 --- a/sway/tree/container.c +++ b/sway/tree/container.c @@ -473,6 +473,7 @@ void container_destroy(struct sway_container *con) { "which is still referenced by transactions")) { return; } + node_clear_dirty(&con->node); free(con->title); free(con->formatted_title); free(con->title_format); @@ -527,7 +528,7 @@ void container_begin_destroy(struct sway_container *con) { container_fullscreen_disable(con); } - wl_signal_emit_mutable(&con->node.events.destroy, &con->node); + node_fini(&con->node); container_end_mouse_operation(con); @@ -1879,6 +1880,10 @@ void container_detach(struct sway_container *child) { struct sway_container *old_parent = child->pending.parent; struct sway_workspace *old_workspace = child->pending.workspace; + + if (old_parent && old_parent->pending.focused_inactive_child == child) { + old_parent->pending.focused_inactive_child = NULL; + } list_t *siblings = container_get_siblings(child); if (siblings) { int index = list_find(siblings, child); diff --git a/sway/tree/layout.c b/sway/tree/layout.c index cf9819e2c..8f8761635 100644 --- a/sway/tree/layout.c +++ b/sway/tree/layout.c @@ -861,7 +861,6 @@ void layout_move_container_to_workspace(struct sway_container *container, struct container_detach_update_parent_fullscreen_layout(parent, container); container_update_representation(parent); node_set_dirty(&parent->node); - container_reap_empty(parent); layout_add_view(workspace, active, container); node_set_dirty(&container->node); } @@ -1047,13 +1046,10 @@ static void insert_children_relative(struct sway_container *container, struct sw static struct sway_container *extract_view(struct sway_container *view) { struct sway_container *parent = view->pending.parent; - list_t *siblings = parent->pending.children; - int idx = list_find(siblings, view); - list_del(siblings, idx); - container_detach_update_parent_fullscreen_layout(parent, view); - container_update_representation(parent); - node_set_dirty(&parent->node); - container_reap_empty(parent); + container_detach(view); + if (parent) { + container_reap_empty(parent); + } return view; } @@ -1390,8 +1386,6 @@ static bool layout_move_container_nomode(struct sway_container *container, enum struct sway_container *active = new_parent->current.focused_inactive_child; int new_index = layout_insert_compute_index(new_parent->pending.children, active, pos); layout_insert_into_container(new_parent, container, new_index); - // Delete old parent container - container_reap_empty(parent); } apply_container_sizes(new_parent, layout_toggle_size_width_fraction(workspace), layout_toggle_size_height_fraction(workspace), OPERATION_FOCUS); @@ -1570,9 +1564,9 @@ static bool filter_container_visible(struct sway_workspace *workspace, maxy = workspace->y + workspace->height; } if (container->pending.x < minx || - container->pending.x + container->pending.width * scale >= maxx || + container->pending.x + container->pending.width * scale > maxx || container->pending.y < miny || - container->pending.y + container->pending.height * scale >= maxy) { + container->pending.y + container->pending.height * scale > maxy) { return false; } return true; diff --git a/sway/tree/node.c b/sway/tree/node.c index f2f6b331c..374c822ec 100644 --- a/sway/tree/node.c +++ b/sway/tree/node.c @@ -1,3 +1,4 @@ +#include #include "sway/output.h" #include "sway/server.h" #include "sway/tree/container.h" @@ -7,6 +8,82 @@ #include "sway/layers.h" #include "log.h" +#include "khashl.h" + +typedef struct sway_node *sway_node_ptr; + +static kh_inline khint_t node_hash_fn(sway_node_ptr node) { + return node->id; +} + +static kh_inline int node_eq_fn(sway_node_ptr a, sway_node_ptr b) { + return a->id == b->id; +} + +KHASHL_INIT(KH_LOCAL, node_map_t, node_kh, sway_node_ptr, node_hash_fn, node_eq_fn) + +static node_map_t *node_table = NULL; + +void node_map_init(void) { + if (node_table) { + sway_abort("node_table already initialized"); + } + node_table = node_kh_init(); + if (!node_table) { + sway_abort("Failed to allocate node map table"); + } +} + +void node_map_fini(void) { + if (!node_table) { + sway_abort("node_table not initialized"); + } + node_kh_destroy(node_table); + node_table = NULL; +} + +static void node_map_add(struct sway_node *node) { + if (!node_table) { + sway_abort("node_table not initialized"); + } + int ret; + node_kh_put(node_table, node, &ret); + if (ret == -1) { + sway_abort("Failed to insert node into hash map (OOM)"); + } +} + +struct sway_node *node_by_id(size_t id) { + if (!node_table) { + return NULL; + } + struct sway_node dummy = {.id = id}; + khint_t k = node_kh_get(node_table, &dummy); + if (k == kh_end(node_table)) { + return NULL; + } + return kh_bucket(node_table, k); +} + +static void node_map_remove(struct sway_node *node) { + if (!node_table) { + sway_abort("node_table not initialized"); + } + khint_t k = node_kh_get(node_table, node); + if (k == kh_end(node_table)) { + sway_abort("Node not present in table"); + } + node_kh_del(node_table, k); +} + +void node_fini(struct sway_node *node) { + node_map_remove(node); + node_clear_dirty(node); + wl_signal_emit_mutable(&node->events.destroy, node); +} + + + void node_init(struct sway_node *node, enum sway_node_type type, void *thing) { static size_t next_id = 1; node->id = next_id++; @@ -14,8 +91,10 @@ void node_init(struct sway_node *node, enum sway_node_type type, void *thing) { node->sway_root = thing; node->focus_warp = FOCUS_WARP_NONE; wl_signal_init(&node->events.destroy); + node_map_add(node); } + const char *node_type_to_str(enum sway_node_type type) { switch (type) { case N_ROOT: @@ -42,6 +121,16 @@ void node_set_dirty(struct sway_node *node) { list_add(server.dirty_nodes, node); } +void node_clear_dirty(struct sway_node *node) { + node->dirty = false; + if (server.dirty_nodes) { + int index = list_find(server.dirty_nodes, node); + if (index != -1) { + list_del(server.dirty_nodes, index); + } + } +} + bool node_is_view(struct sway_node *node) { return node->type == N_CONTAINER && node->sway_container->view; } diff --git a/sway/tree/output.c b/sway/tree/output.c index 887684de7..d91658b9b 100644 --- a/sway/tree/output.c +++ b/sway/tree/output.c @@ -271,6 +271,7 @@ void output_destroy(struct sway_output *output) { "which is still referenced by transactions")) { return; } + node_clear_dirty(&output->node); destroy_scene_layers(output); list_free(output->workspaces); @@ -308,7 +309,7 @@ void output_begin_destroy(struct sway_output *output) { return; } sway_log(SWAY_DEBUG, "Destroying output '%s'", output->wlr_output->name); - wl_signal_emit_mutable(&output->node.events.destroy, &output->node); + node_fini(&output->node); node_set_dirty(&output->node); output->node.destroying = true; diff --git a/sway/tree/root.c b/sway/tree/root.c index 1e6b396f2..9499d0c7a 100644 --- a/sway/tree/root.c +++ b/sway/tree/root.c @@ -14,6 +14,8 @@ #include "sway/tree/container.h" #include "sway/tree/root.h" #include "sway/tree/workspace.h" +#include "sway/tree/node.h" +#include "sway/tree/space.h" #include "list.h" #include "log.h" #include "util.h" @@ -89,13 +91,70 @@ struct sway_root *root_create(struct wl_display *wl_display) { return root; } +static void container_destroy_recursive(struct sway_container *con) { + if (con->pending.children) { + for (int i = 0; i < con->pending.children->length; ++i) { + container_destroy_recursive(con->pending.children->items[i]); + } + } + con->node.destroying = true; + con->node.ntxnrefs = 0; + container_destroy(con); +} + +static void workspace_destroy_recursive(struct sway_workspace *ws) { + if (ws->tiling) { + for (int i = 0; i < ws->tiling->length; ++i) { + container_destroy_recursive(ws->tiling->items[i]); + } + } + if (ws->floating) { + for (int i = 0; i < ws->floating->length; ++i) { + container_destroy_recursive(ws->floating->items[i]); + } + } + ws->node.destroying = true; + ws->node.ntxnrefs = 0; + workspace_destroy(ws); +} + +static void output_destroy_recursive(struct sway_output *output) { + if (output->workspaces) { + for (int i = 0; i < output->workspaces->length; ++i) { + workspace_destroy_recursive(output->workspaces->items[i]); + } + } + output->node.destroying = true; + output->node.ntxnrefs = 0; + output->wlr_output = NULL; + output_destroy(output); +} + void root_destroy(struct sway_root *root) { + while (root->spaces->length > 0) { + struct sway_space *space = root->spaces->items[0]; + space_delete(space->name); + } list_free(root->spaces); + + if (root->scratchpad) { + for (int i = 0; i < root->scratchpad->length; ++i) { + container_destroy_recursive(root->scratchpad->items[i]); + } + } list_free(root->scratchpad); + + if (root->fallback_output) { + root->fallback_output->wlr_output = NULL; + root->fallback_output->node.destroying = true; + output_destroy_recursive(root->fallback_output); + } + list_free(root->non_desktop_outputs); list_free(root->outputs); wlr_scene_node_destroy(&root->root_scene->tree.node); list_free_items_and_destroy(root->filters_list); + node_fini(&root->node); free(root); } @@ -126,6 +185,9 @@ void root_scratchpad_add_container(struct sway_container *con, struct sway_works struct sway_container *parent = con->pending.parent; struct sway_workspace *workspace = con->pending.workspace; + size_t parent_id = parent ? parent->node.id : 0; + bool was_tiled = !container_is_floating(con); + set_container_transform(workspace, con); // Clear the fullscreen mode when sending to the scratchpad @@ -134,17 +196,24 @@ void root_scratchpad_add_container(struct sway_container *con, struct sway_works } // When a tiled window is sent to scratchpad, center and resize it. - if (!container_is_floating(con)) { + if (was_tiled) { container_set_floating(con, true); container_floating_set_default_size(con); container_floating_move_to_center(con); } container_detach(con); - if (parent && parent->pending.children->length == 0) { - container_reap_empty(parent); - parent = NULL; + + // parent might have been reaped and destroyed in container_set_floating. + // Verify it still exists before using it. + parent = NULL; + if (parent_id) { + struct sway_node *node = node_by_id(parent_id); + if (node && node->type == N_CONTAINER) { + parent = node->sway_container; + } } + con->scratchpad = true; list_add(root->scratchpad, con); if (ws) { diff --git a/sway/tree/space.c b/sway/tree/space.c index b37cb3fdf..934758596 100644 --- a/sway/tree/space.c +++ b/sway/tree/space.c @@ -1,29 +1,25 @@ #include "sway/tree/space.h" #include "sway/tree/workspace.h" +#include "sway/tree/node.h" #include "sway/tree/arrange.h" #include "sway/output.h" -static bool find_view(struct sway_container *container, void *data) { - struct sway_view *view = data; - return container->view == view; -} - -// Find the view. If it exists, detach its container and return it. If it -// doesn't, return NULL -static struct sway_container *find_and_detach_container(struct sway_view *view) { - struct sway_container *container = root_find_container(find_view, view); - if (container) { - if (!container_is_floating(container)) { - struct sway_container *parent = container->pending.parent; - list_t *siblings = parent->pending.children; - list_del(siblings, list_find(siblings, container)); - node_set_dirty(&parent->pending.workspace->node); - container_update_representation(parent); - node_set_dirty(&parent->node); - container_reap_empty(parent); +static struct sway_container *find_and_detach_container_by_id(size_t id) { + struct sway_node *node = node_by_id(id); + if (node && node->type == N_CONTAINER) { + struct sway_container *container = node->sway_container; + if (container) { + if (!container_is_floating(container)) { + struct sway_container *parent = container->pending.parent; + container_detach(container); + if (parent) { + container_reap_empty(parent); + } + } } + return container; } - return container; + return NULL; } static void fill_container(struct sway_space_container *space_container, @@ -79,7 +75,7 @@ static struct sway_container *layout_space_container_restore_tiling(struct sway_ } if (space_container->view) { // Find view, detach its container - struct sway_container *container = find_and_detach_container(space_container->view->view); + struct sway_container *container = find_and_detach_container_by_id(space_container->view->container_id); if (container) { if (container->scratchpad) { root_scratchpad_show(container); @@ -90,14 +86,14 @@ static struct sway_container *layout_space_container_restore_tiling(struct sway_ list_del(ws->floating, list_find(ws->floating, container)); workspace_consider_destroy(ws); node_set_dirty(&ws->node); - list_add(view_float, space_container->view->view); + list_add(view_float, container->view); } container->view->content_scale = space_container->view->content_scale; container->pending.workspace = parent->pending.workspace; + container->pending.parent = parent; arrange_container(container); node_set_dirty(&container->node); list_add(parent->pending.children, container); - container->pending.parent = parent; fill_container(space_container, container); container_update_representation(container); node_set_dirty(&parent->node); @@ -120,7 +116,7 @@ static void layout_space_container_restore_floating(struct sway_workspace *works struct sway_space_container *focused, list_t *view_float) { if (space_container->view) { // Find view, detach its container - struct sway_container *container = find_and_detach_container(space_container->view->view); + struct sway_container *container = find_and_detach_container_by_id(space_container->view->container_id); if (container) { if (container->scratchpad) { root_scratchpad_show(container); @@ -135,11 +131,11 @@ static void layout_space_container_restore_floating(struct sway_workspace *works list_add(view_float, container->view); } container->view->content_scale = space_container->view->content_scale; + container->pending.parent = NULL; + container->pending.workspace = workspace; arrange_container(container); node_set_dirty(&container->node); list_add(workspace->floating, container); - container->pending.parent = NULL; - container->pending.workspace = workspace; fill_container(space_container, container); container_update_representation(container); } @@ -163,7 +159,7 @@ static bool container_find_view(struct sway_space_container *container, return true; } } - } else if (container->view->view == view) { + } else if (container->view->container_id == (view->container ? view->container->node.id : 0)) { return true; } return false; @@ -249,10 +245,10 @@ void layout_space_restore(struct sway_space *space, struct sway_workspace *works list_free(view_float); } -static struct sway_space_view *space_view_create(struct sway_view *sway_view, +static struct sway_space_view *space_view_create(size_t container_id, struct sway_space_container *container, float content_scale) { struct sway_space_view *view = malloc(sizeof(struct sway_space_view)); - view->view = sway_view; + view->container_id = container_id; view->container = container; view->content_scale = content_scale; return view; @@ -283,7 +279,7 @@ static struct sway_space_container *space_container_create(struct sway_container space_container->children = NULL; } if (container->view) { - space_container->view = space_view_create(container->view, space_container, + space_container->view = space_view_create(container->node.id, space_container, container->view->content_scale); } else { space_container->view = NULL; diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c index 153721e59..5df40566c 100644 --- a/sway/tree/workspace.c +++ b/sway/tree/workspace.c @@ -302,6 +302,7 @@ void workspace_destroy(struct sway_workspace *workspace) { "which is still referenced by transactions")) { return; } + node_clear_dirty(&workspace->node); scene_node_disown_children(workspace->layers.tiling); scene_node_disown_children(workspace->layers.fullscreen); @@ -325,7 +326,16 @@ void workspace_destroy(struct sway_workspace *workspace) { void workspace_begin_destroy(struct sway_workspace *workspace) { sway_log(SWAY_DEBUG, "Destroying workspace '%s'", workspace->name); ipc_event_workspace(NULL, workspace, "empty"); // intentional - wl_signal_emit_mutable(&workspace->node.events.destroy, &workspace->node); + if (workspace->split.split != WORKSPACE_SPLIT_NONE) { + struct sway_workspace *sibling = workspace->split.sibling; + sibling->split.split = WORKSPACE_SPLIT_NONE; + sibling->layers.tiling->node.info.output_box = NULL; + sibling->split.sibling = NULL; + node_set_dirty(&sibling->node); + arrange_workspace(sibling); + } + + node_fini(&workspace->node); wlr_ext_workspace_handle_v1_destroy(workspace->ext_workspace); workspace->ext_workspace = NULL; @@ -796,6 +806,10 @@ struct workspace_switch_data { struct sway_root_filters *root_filters; }; +static void commit_dirty_idle_callback(void *data) { + transaction_commit_dirty(); +} + static void workspace_switch_callback_end(void *callback_data) { struct workspace_switch_data *data = callback_data; root_filters_destroy(root, data->root_filters); @@ -822,7 +836,7 @@ static void workspace_switch_callback_end(void *callback_data) { list_free_items_and_destroy(data->to_containers); free(data); - transaction_commit_dirty(); + wl_event_loop_add_idle(server.wl_event_loop, commit_dirty_idle_callback, NULL); } static bool workspace_switch_output_fullscreen_filter(struct sway_output *output, diff --git a/swaybar/main.c b/swaybar/main.c index 5fe8596f1..9d11fee58 100644 --- a/swaybar/main.c +++ b/swaybar/main.c @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #include "swaybar/bar.h" #include "ipc-client.h" #include "log.h" @@ -94,5 +97,8 @@ int main(int argc, char **argv) { swaybar.running = true; bar_run(&swaybar); bar_teardown(&swaybar); + pango_cairo_font_map_set_default(NULL); + cairo_debug_reset_static_data(); + FcFini(); return 0; } diff --git a/swaybar/meson.build b/swaybar/meson.build index 3f6176409..6b814a48f 100644 --- a/swaybar/meson.build +++ b/swaybar/meson.build @@ -8,6 +8,7 @@ tray_files = have_tray ? [ swaybar_deps = [ cairo, + fontconfig, gdk_pixbuf, jsonc, math, diff --git a/swaybar/status_line.c b/swaybar/status_line.c index e542e606b..0e7ca6ed3 100644 --- a/swaybar/status_line.c +++ b/swaybar/status_line.c @@ -65,50 +65,52 @@ bool status_handle_readable(struct status_line *status) { // the header must be sent completely the first time round char *newline = strchr(status->buffer, '\n'); - json_object *header, *version; - if (newline != NULL - && (header = json_tokener_parse(status->buffer)) - && json_object_object_get_ex(header, "version", &version) - && json_object_get_int(version) == 1) { - sway_log(SWAY_DEBUG, "Using i3bar protocol."); - status->protocol = PROTOCOL_I3BAR; - - json_object *click_events; - if (json_object_object_get_ex(header, "click_events", &click_events) - && json_object_get_boolean(click_events)) { - sway_log(SWAY_DEBUG, "Enabling click events."); - status->click_events = true; - if (write(status->write_fd, "[\n", 2) != 2) { - status_error(status, "[failed to write to status command]"); - json_object_put(header); - return true; + json_object *header = NULL, *version; + if (newline != NULL && (header = json_tokener_parse(status->buffer))) { + if (json_object_object_get_ex(header, "version", &version) + && json_object_get_int(version) == 1) { + sway_log(SWAY_DEBUG, "Using i3bar protocol."); + status->protocol = PROTOCOL_I3BAR; + + json_object *click_events; + if (json_object_object_get_ex(header, "click_events", &click_events) + && json_object_get_boolean(click_events)) { + sway_log(SWAY_DEBUG, "Enabling click events."); + status->click_events = true; + if (write(status->write_fd, "[\n", 2) != 2) { + status_error(status, "[failed to write to status command]"); + json_object_put(header); + return true; + } } - } - json_object *float_event_coords; - if (json_object_object_get_ex(header, "float_event_coords", &float_event_coords) - && json_object_get_boolean(float_event_coords)) { - sway_log(SWAY_DEBUG, "Enabling floating-point coordinates."); - status->float_event_coords = true; - } + json_object *float_event_coords; + if (json_object_object_get_ex(header, "float_event_coords", &float_event_coords) + && json_object_get_boolean(float_event_coords)) { + sway_log(SWAY_DEBUG, "Enabling floating-point coordinates."); + status->float_event_coords = true; + } - json_object *signal; - if (json_object_object_get_ex(header, "stop_signal", &signal)) { - status->stop_signal = json_object_get_int(signal); - sway_log(SWAY_DEBUG, "Setting stop signal to %d", status->stop_signal); - } - if (json_object_object_get_ex(header, "cont_signal", &signal)) { - status->cont_signal = json_object_get_int(signal); - sway_log(SWAY_DEBUG, "Setting cont signal to %d", status->cont_signal); - } + json_object *signal; + if (json_object_object_get_ex(header, "stop_signal", &signal)) { + status->stop_signal = json_object_get_int(signal); + sway_log(SWAY_DEBUG, "Setting stop signal to %d", status->stop_signal); + } + if (json_object_object_get_ex(header, "cont_signal", &signal)) { + status->cont_signal = json_object_get_int(signal); + sway_log(SWAY_DEBUG, "Setting cont signal to %d", status->cont_signal); + } - json_object_put(header); + json_object_put(header); - wl_list_init(&status->blocks); - status->tokener = json_tokener_new(); - status->buffer_index = strlen(newline + 1); - memmove(status->buffer, newline + 1, status->buffer_index + 1); - return i3bar_handle_readable(status); + wl_list_init(&status->blocks); + status->tokener = json_tokener_new(); + status->buffer_index = strlen(newline + 1); + memmove(status->buffer, newline + 1, status->buffer_index + 1); + return i3bar_handle_readable(status); + } else { + json_object_put(header); + } } sway_log(SWAY_DEBUG, "Using text protocol."); diff --git a/swaybar/tray/host.c b/swaybar/tray/host.c index 79b54606f..498d5272e 100644 --- a/swaybar/tray/host.c +++ b/swaybar/tray/host.c @@ -143,8 +143,7 @@ bool init_host(struct swaybar_host *host, char *protocol, return false; } - sd_bus_slot *reg_slot = NULL, *unreg_slot = NULL, *watcher_slot = NULL; - int ret = sd_bus_match_signal(tray->bus, ®_slot, host->watcher_interface, + int ret = sd_bus_match_signal(tray->bus, &host->reg_slot, host->watcher_interface, watcher_path, host->watcher_interface, "StatusNotifierItemRegistered", handle_sni_registered, tray); if (ret < 0) { @@ -152,7 +151,7 @@ bool init_host(struct swaybar_host *host, char *protocol, strerror(-ret)); goto error; } - ret = sd_bus_match_signal(tray->bus, &unreg_slot, host->watcher_interface, + ret = sd_bus_match_signal(tray->bus, &host->unreg_slot, host->watcher_interface, watcher_path, host->watcher_interface, "StatusNotifierItemUnregistered", handle_sni_unregistered, tray); if (ret < 0) { @@ -161,7 +160,7 @@ bool init_host(struct swaybar_host *host, char *protocol, goto error; } - ret = sd_bus_match_signal(tray->bus, &watcher_slot, "org.freedesktop.DBus", + ret = sd_bus_match_signal(tray->bus, &host->watcher_slot, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameOwnerChanged", handle_new_watcher, host); if (ret < 0) { @@ -186,21 +185,20 @@ bool init_host(struct swaybar_host *host, char *protocol, goto error; } - sd_bus_slot_set_floating(reg_slot, 0); - sd_bus_slot_set_floating(unreg_slot, 0); - sd_bus_slot_set_floating(watcher_slot, 0); - sway_log(SWAY_DEBUG, "Registered %s", host->service); return true; error: - sd_bus_slot_unref(reg_slot); - sd_bus_slot_unref(unreg_slot); - sd_bus_slot_unref(watcher_slot); + sd_bus_slot_unref(host->reg_slot); + sd_bus_slot_unref(host->unreg_slot); + sd_bus_slot_unref(host->watcher_slot); finish_host(host); return false; } void finish_host(struct swaybar_host *host) { + sd_bus_slot_unref(host->reg_slot); + sd_bus_slot_unref(host->unreg_slot); + sd_bus_slot_unref(host->watcher_slot); sd_bus_release_name(host->tray->bus, host->service); free(host->service); free(host->watcher_interface); diff --git a/swaybar/tray/item.c b/swaybar/tray/item.c index af9b5cde3..010b11b35 100644 --- a/swaybar/tray/item.c +++ b/swaybar/tray/item.c @@ -167,6 +167,7 @@ static int get_property_callback(sd_bus_message *msg, void *data, } cleanup: wl_list_remove(&d->link); + sd_bus_slot_unref(d->slot); free(data); return ret; } diff --git a/swaybar/tray/watcher.c b/swaybar/tray/watcher.c index 284964030..13d5dc4d0 100644 --- a/swaybar/tray/watcher.c +++ b/swaybar/tray/watcher.c @@ -171,15 +171,14 @@ struct swaybar_watcher *create_watcher(char *protocol, sd_bus *bus) { watcher->interface = format_str("org.%s.StatusNotifierWatcher", protocol); - sd_bus_slot *signal_slot = NULL, *vtable_slot = NULL; - int ret = sd_bus_add_object_vtable(bus, &vtable_slot, obj_path, + int ret = sd_bus_add_object_vtable(bus, &watcher->vtable_slot, obj_path, watcher->interface, watcher_vtable, watcher); if (ret < 0) { sway_log(SWAY_ERROR, "Failed to add object vtable: %s", strerror(-ret)); goto error; } - ret = sd_bus_match_signal(bus, &signal_slot, "org.freedesktop.DBus", + ret = sd_bus_match_signal(bus, &watcher->signal_slot, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameOwnerChanged", handle_lost_service, watcher); if (ret < 0) { @@ -200,9 +199,6 @@ struct swaybar_watcher *create_watcher(char *protocol, sd_bus *bus) { goto error; } - sd_bus_slot_set_floating(signal_slot, 0); - sd_bus_slot_set_floating(vtable_slot, 0); - watcher->bus = bus; watcher->hosts = create_list(); watcher->items = create_list(); @@ -210,8 +206,8 @@ struct swaybar_watcher *create_watcher(char *protocol, sd_bus *bus) { sway_log(SWAY_DEBUG, "Registered %s", watcher->interface); return watcher; error: - sd_bus_slot_unref(signal_slot); - sd_bus_slot_unref(vtable_slot); + sd_bus_slot_unref(watcher->signal_slot); + sd_bus_slot_unref(watcher->vtable_slot); destroy_watcher(watcher); return NULL; } @@ -220,6 +216,8 @@ void destroy_watcher(struct swaybar_watcher *watcher) { if (!watcher) { return; } + sd_bus_slot_unref(watcher->signal_slot); + sd_bus_slot_unref(watcher->vtable_slot); list_free_items_and_destroy(watcher->hosts); list_free_items_and_destroy(watcher->items); free(watcher->interface); diff --git a/tests/clients/wayland-client.c b/tests/clients/wayland-client.c new file mode 100644 index 000000000..9aad66e2e --- /dev/null +++ b/tests/clients/wayland-client.c @@ -0,0 +1,148 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include "xdg-shell-client-protocol.h" + +struct client_state { + struct wl_display *display; + struct wl_registry *registry; + struct wl_compositor *compositor; + struct wl_shm *shm; + struct xdg_wm_base *xdg_wm_base; + struct wl_surface *surface; + struct xdg_surface *xdg_surface; + struct xdg_toplevel *xdg_toplevel; + struct wl_buffer *buffer; + int width, height; + void *shm_data; +}; + +static void noop() {} + +static void shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) {} +static const struct wl_shm_listener shm_listener = { shm_format }; + +static void xdg_wm_base_ping(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial) { + xdg_wm_base_pong(xdg_wm_base, serial); +} +static const struct xdg_wm_base_listener xdg_wm_base_listener = { xdg_wm_base_ping }; + +static void registry_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { + struct client_state *state = data; + if (strcmp(interface, wl_compositor_interface.name) == 0) { + state->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 4); + } else if (strcmp(interface, wl_shm_interface.name) == 0) { + state->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1); + wl_shm_add_listener(state->shm, &shm_listener, state); + } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) { + state->xdg_wm_base = wl_registry_bind(registry, name, &xdg_wm_base_interface, 1); + xdg_wm_base_add_listener(state->xdg_wm_base, &xdg_wm_base_listener, state); + } +} +static void registry_global_remove(void *data, struct wl_registry *registry, uint32_t name) {} +static const struct wl_registry_listener registry_listener = { registry_global, registry_global_remove }; + +static void xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial) { + struct client_state *state = data; + xdg_surface_ack_configure(xdg_surface, serial); + + if (!state->buffer) { + int stride = state->width * 4; + int size = stride * state->height; + + int fd = memfd_create("scroll-test-shm", MFD_CLOEXEC); + if (fd < 0) { + perror("memfd_create"); + exit(1); + } + if (ftruncate(fd, size) < 0) { + perror("ftruncate"); + exit(1); + } + state->shm_data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (state->shm_data == MAP_FAILED) { + perror("mmap"); + exit(1); + } + struct wl_shm_pool *pool = wl_shm_create_pool(state->shm, fd, size); + state->buffer = wl_shm_pool_create_buffer(pool, 0, state->width, state->height, stride, WL_SHM_FORMAT_XRGB8888); + wl_shm_pool_destroy(pool); + close(fd); + + uint32_t *pixels = state->shm_data; + for (int i = 0; i < state->width * state->height; ++i) { + pixels[i] = 0xFF0000FF; // Blue + } + } + + wl_surface_attach(state->surface, state->buffer, 0, 0); + wl_surface_damage_buffer(state->surface, 0, 0, state->width, state->height); + wl_surface_commit(state->surface); +} +static const struct xdg_surface_listener xdg_surface_listener = { xdg_surface_configure }; + +static void xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height, struct wl_array *states) {} +static void xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel) { + exit(0); +} +static const struct xdg_toplevel_listener xdg_toplevel_listener = { xdg_toplevel_configure, xdg_toplevel_close, noop, noop }; + +int main(int argc, char **argv) { + struct client_state state = {0}; + state.width = 100; + state.height = 100; + + const char *title = "Test Wayland Window"; + const char *app_id = "test_app_id"; + if (argc > 1) title = argv[1]; + if (argc > 2) app_id = argv[2]; + + state.display = wl_display_connect(NULL); + if (!state.display) { + fprintf(stderr, "Failed to connect to Wayland display\n"); + return 1; + } + + state.registry = wl_display_get_registry(state.display); + wl_registry_add_listener(state.registry, ®istry_listener, &state); + wl_display_roundtrip(state.display); + + if (!state.compositor || !state.shm || !state.xdg_wm_base) { + fprintf(stderr, "Missing globals\n"); + return 1; + } + + state.surface = wl_compositor_create_surface(state.compositor); + state.xdg_surface = xdg_wm_base_get_xdg_surface(state.xdg_wm_base, state.surface); + xdg_surface_add_listener(state.xdg_surface, &xdg_surface_listener, &state); + + state.xdg_toplevel = xdg_surface_get_toplevel(state.xdg_surface); + xdg_toplevel_add_listener(state.xdg_toplevel, &xdg_toplevel_listener, &state); + + xdg_toplevel_set_title(state.xdg_toplevel, title); + xdg_toplevel_set_app_id(state.xdg_toplevel, app_id); + + wl_surface_commit(state.surface); + + while (wl_display_dispatch(state.display) != -1) { + // Loop + } + + if (state.buffer) wl_buffer_destroy(state.buffer); + if (state.shm_data) munmap(state.shm_data, state.width * 4 * state.height); + if (state.xdg_toplevel) xdg_toplevel_destroy(state.xdg_toplevel); + if (state.xdg_surface) xdg_surface_destroy(state.xdg_surface); + if (state.surface) wl_surface_destroy(state.surface); + if (state.compositor) wl_compositor_destroy(state.compositor); + if (state.shm) wl_shm_destroy(state.shm); + if (state.xdg_wm_base) xdg_wm_base_destroy(state.xdg_wm_base); + if (state.registry) wl_registry_destroy(state.registry); + if (state.display) wl_display_disconnect(state.display); + + return 0; +} diff --git a/tests/clients/x11-client.c b/tests/clients/x11-client.c new file mode 100644 index 000000000..6357f3d88 --- /dev/null +++ b/tests/clients/x11-client.c @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include + +int main(int argc, char **argv) { + xcb_connection_t *conn = xcb_connect(NULL, NULL); + if (xcb_connection_has_error(conn)) { + fprintf(stderr, "Failed to connect to X11 display\n"); + return 1; + } + + xcb_screen_t *screen = xcb_setup_roots_iterator(xcb_get_setup(conn)).data; + xcb_window_t win = xcb_generate_id(conn); + + uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; + uint32_t values[2] = { + screen->white_pixel, + XCB_EVENT_MASK_EXPOSURE + }; + + xcb_create_window(conn, XCB_COPY_FROM_PARENT, win, screen->root, + 0, 0, 150, 150, 10, + XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, + mask, values); + + const char *title = "Test X11 Window"; + if (argc > 1) title = argv[1]; + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, win, + XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, + strlen(title), title); + + const char *instance = "test_instance"; + const char *class = "TestClass"; + if (argc > 2) instance = argv[2]; + if (argc > 3) class = argv[3]; + size_t class_len = strlen(instance) + 1 + strlen(class) + 1; + char *class_str = malloc(class_len); + if (!class_str) { + perror("malloc"); + return 1; + } + strcpy(class_str, instance); + strcpy(class_str + strlen(instance) + 1, class); + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, win, + XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 8, + class_len, class_str); + free(class_str); + + xcb_map_window(conn, win); + xcb_flush(conn); + + while (1) { + xcb_generic_event_t *ev = xcb_wait_for_event(conn); + if (!ev) break; + free(ev); + } + + xcb_disconnect(conn); + return 0; +} diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..4531b51cd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,71 @@ +import subprocess +import os +import pytest +from typing import Generator +from pathlib import Path +from test_utils import ScrollInstance, run_compositor + + +def pytest_addoption(parser: pytest.Parser) -> None: + parser.addoption("--scroll", help="the scroll binary to test", default=None) + + +@pytest.fixture(scope="session") +def scroll_compositor_binary(request: pytest.FixtureRequest) -> str: + binary_path: str = request.config.getoption("scroll") + if not binary_path: + # Auto-build using Meson/Ninja + print("\nBuilding scroll with Meson/Ninja...") + build_dir = os.path.abspath("./build") + if not os.path.exists(build_dir): + res = subprocess.run( + ["meson", "setup", "build", "-Dwerror=false", "-Db_sanitize=address"], + capture_output=True, + text=True, + ) + if res.returncode != 0: + pytest.exit( + f"Failed to setup build:\nStdout: {res.stdout}\nStderr: {res.stderr}" + ) + else: + # Ensure ASan is enabled + res = subprocess.run( + ["meson", "configure", "build", "-Db_sanitize=address"], + capture_output=True, + text=True, + ) + if res.returncode != 0: + pytest.exit( + f"Failed to configure build with ASan:\nStdout: {res.stdout}\nStderr: {res.stderr}" + ) + + # Run ninja to compile (incremental build) + res = subprocess.run(["ninja", "-C", "build"], capture_output=True, text=True) + if res.returncode != 0: + pytest.exit( + f"Failed to build scroll:\nStdout: {res.stdout}\nStderr: {res.stderr}" + ) + + binary_path = os.path.join(build_dir, "sway", "scroll") + else: + binary_path = os.path.abspath(binary_path) + + assert os.path.exists(binary_path), f"Binary not found at {binary_path}" + return binary_path + + +@pytest.fixture(scope="session") +def scroll_compositor( + scroll_compositor_binary: str, tmp_path_factory: pytest.TempPathFactory +) -> Generator[ScrollInstance, None, None]: + temp_dir: Path = tmp_path_factory.mktemp("scroll") + with run_compositor(scroll_compositor_binary, temp_dir) as inst: + yield inst + + +@pytest.fixture(scope="function") +def fresh_compositor( + scroll_compositor_binary: str, tmp_path: Path +) -> Generator[ScrollInstance, None, None]: + with run_compositor(scroll_compositor_binary, tmp_path) as inst: + yield inst diff --git a/tests/lsan.supp b/tests/lsan.supp new file mode 100644 index 000000000..e01775a38 --- /dev/null +++ b/tests/lsan.supp @@ -0,0 +1,2 @@ +# Leaks in EGL/DRM initialization due to missing eglTerminate in wlroots (or system libraries) +leak:egl_init_display diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 000000000..2b153d5df --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,17 @@ +executable( + 'wayland-test-client', + files('clients/wayland-client.c'), + dependencies: [wayland_client], + sources: wl_protos_src, + install: false, +) + +xcb_dep = dependency('xcb', required: false) +if xcb_dep.found() + executable( + 'x11-test-client', + files('clients/x11-client.c'), + dependencies: [xcb_dep], + install: false, + ) +endif diff --git a/tests/scrollipc.py b/tests/scrollipc.py new file mode 100644 index 000000000..4d999f271 --- /dev/null +++ b/tests/scrollipc.py @@ -0,0 +1,63 @@ +import socket +import struct +import json + +MAGIC: bytes = b"i3-ipc" +HEADER_FORMAT: str = "<6sII" # 6 bytes magic, 4 bytes length, 4 bytes type +HEADER_SIZE: int = struct.calcsize(HEADER_FORMAT) + +# Command types from include/ipc.h +IPC_COMMAND: int = 0 +IPC_GET_WORKSPACES: int = 1 +IPC_SUBSCRIBE: int = 2 +IPC_GET_VERSION: int = 7 + + +class ScrollIPC: + socket_path: str + sock: socket.socket + + def __init__(self, socket_path: str): + self.socket_path = socket_path + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.connect(socket_path) + + def close(self) -> None: + self.sock.close() + + def _send(self, msg_type: int, payload: str) -> None: + payload_bytes: bytes = payload.encode("utf-8") + length: int = len(payload_bytes) + header: bytes = struct.pack(HEADER_FORMAT, MAGIC, length, msg_type) + self.sock.sendall(header + payload_bytes) + + def _recv(self) -> tuple[int, str]: + header_data: bytes = self._recv_all(HEADER_SIZE) + magic: bytes + length: int + msg_type: int + magic, length, msg_type = struct.unpack(HEADER_FORMAT, header_data) + if magic != MAGIC: + raise ValueError(f"Invalid magic string: {magic}") + payload_data: bytes = self._recv_all(length) + return msg_type, payload_data.decode("utf-8") + + def _recv_all(self, size: int) -> bytes: + data: bytes = b"" + while len(data) < size: + chunk: bytes = self.sock.recv(size - len(data)) + if not chunk: + raise EOFError("Socket closed prematurely") + data += chunk + return data + + def command(self, cmd_string: str) -> list: + self._send(IPC_COMMAND, cmd_string) + reply_type: int + reply_payload: str + reply_type, reply_payload = self._recv() + if reply_type != IPC_COMMAND: + raise ValueError(f"Unexpected reply type: {reply_type}") + result = json.loads(reply_payload) + assert isinstance(result, list) + return result diff --git a/tests/test_clients.py b/tests/test_clients.py new file mode 100644 index 000000000..b29a7c690 --- /dev/null +++ b/tests/test_clients.py @@ -0,0 +1,135 @@ +import os +import subprocess +import time +from pathlib import Path +import pytest +from conftest import ScrollInstance + + +def test_wayland_client(scroll_compositor: ScrollInstance) -> None: + wayland_display: str | None = scroll_compositor.getenv("WAYLAND_DISPLAY") + assert wayland_display is not None + + client_path: Path = Path("./build/tests/wayland-test-client").resolve() + assert client_path.exists(), f"Client not found at {client_path}" + + title: str = "My Wayland Test Window" + app_id: str = "my_wayland_app_id" + + env: dict = os.environ.copy() + env["WAYLAND_DISPLAY"] = wayland_display + + proc: subprocess.Popen = subprocess.Popen( + [str(client_path), title, app_id], env=env + ) + + view_info: dict | None = None + tries: int = 0 + while tries < 50: + view_info = scroll_compositor.execute_lua(""" + local view = scroll.focused_view() + if view then + return { + id = view, + title = scroll.view_get_title(view), + app_id = scroll.view_get_app_id(view) + } + end + """) + if ( + view_info + and view_info.get("title") == title + and view_info.get("app_id") == app_id + ): + break + + time.sleep(0.1) + tries += 1 + + assert tries < 50, "Timed out waiting for client to map or verify" + assert view_info is not None + + scroll_compositor.execute_lua(f"scroll.view_close({view_info['id']})") + + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + pytest.fail("Client did not exit after view_close") + + assert proc.returncode == 0 + + +def test_x11_client(scroll_compositor: ScrollInstance) -> None: + display: str | None = scroll_compositor.getenv("DISPLAY") + if not display: + pytest.skip("Xwayland is not enabled (no DISPLAY env var in compositor)") + + xauthority: str | None = scroll_compositor.getenv("XAUTHORITY") + + # Wait for Xwayland to be ready + xwayland_ready_tries: int = 0 + while xwayland_ready_tries < 50: + if "Xserver is ready" in scroll_compositor.read_log(): + break + time.sleep(0.1) + xwayland_ready_tries += 1 + assert xwayland_ready_tries < 50, "Timed out waiting for Xwayland to be ready" + + client_path: Path = Path("./build/tests/x11-test-client").resolve() + if not client_path.exists(): + pytest.skip("X11 test client not built") + + title: str = "My X11 Test Window" + instance: str = "my_x11_instance" + class_name: str = "MyX11Class" + + env: dict = os.environ.copy() + env["DISPLAY"] = display + if xauthority: + env["XAUTHORITY"] = xauthority + + proc: subprocess.Popen = subprocess.Popen( + [str(client_path), title, instance, class_name], env=env + ) + + view_info: dict | None = None + tries: int = 0 + while tries < 50: + view_info = scroll_compositor.execute_lua(""" + local view = scroll.focused_view() + if view then + return { + id = view, + title = scroll.view_get_title(view), + class = scroll.view_get_class(view), + shell = scroll.view_get_shell(view) + } + end + """) + if ( + view_info + and view_info.get("title") == title + and view_info.get("class") == class_name + ): + assert view_info.get("shell") == "xwayland" + break + + time.sleep(0.1) + tries += 1 + + if tries >= 50: + Path("build/test_x11_compositor.log").write_text(scroll_compositor.read_log()) + print("Wrote compositor log to build/test_x11_compositor.log") + assert tries < 50, "Timed out waiting for X11 client to map or verify" + assert view_info is not None + + scroll_compositor.execute_lua(f"scroll.view_close({view_info['id']})") + + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + pytest.fail("X11 client did not exit after view_close") + + assert proc.returncode == 0 diff --git a/tests/test_crash_move.py b/tests/test_crash_move.py new file mode 100644 index 000000000..5250a77a5 --- /dev/null +++ b/tests/test_crash_move.py @@ -0,0 +1,23 @@ +from conftest import ScrollInstance +from test_utils import wayland_client, wait_for_client_map + + +def test_move_left_nomode_no_crash(scroll_compositor: ScrollInstance) -> None: + # Open first window + with wayland_client(scroll_compositor, "Window 1"): + wait_for_client_map(scroll_compositor, "Window 1") + + # Open second window + with wayland_client(scroll_compositor, "Window 2"): + wait_for_client_map(scroll_compositor, "Window 2") + + # Both windows are open. Now move left nomode. + try: + res = scroll_compositor.cmd("move left nomode") + assert res and res[0]["success"], f"Command failed: {res}" + except Exception as e: + print(f"Compositor log:\n{scroll_compositor.read_log()}") + raise e + + # Check if compositor process is still alive + assert scroll_compositor.proc.poll() is None, "Compositor crashed" diff --git a/tests/test_focused_inactive_uaf.py b/tests/test_focused_inactive_uaf.py new file mode 100644 index 000000000..1a19e5fc6 --- /dev/null +++ b/tests/test_focused_inactive_uaf.py @@ -0,0 +1,75 @@ +import time +from conftest import ScrollInstance +from test_utils import wayland_client, wait_for_client_map + + +def test_focused_inactive_uaf(fresh_compositor: ScrollInstance) -> None: + # 1. Set mode to vertical to stack windows + fresh_compositor.cmd("set_mode v") + + # 2. Create first view + with wayland_client(fresh_compositor, "client1") as client1: + wait_for_client_map(fresh_compositor, "client1") + w1_id = fresh_compositor.execute_lua("return scroll.focused_container()") + print(f"w1_id: {w1_id}") + + # 3. Create second view + with wayland_client(fresh_compositor, "client2"): + wait_for_client_map(fresh_compositor, "client2") + w2_id = fresh_compositor.execute_lua("return scroll.focused_container()") + print(f"w2_id: {w2_id}") + + # Verify they are siblings (same parent) + w1_parent = fresh_compositor.execute_lua( + f"return scroll.container_get_parent({w1_id})" + ) + w2_parent = fresh_compositor.execute_lua( + f"return scroll.container_get_parent({w2_id})" + ) + print(f"w1_parent: {w1_parent}, w2_parent: {w2_parent}") + assert w1_parent == w2_parent, "w1 and w2 should be siblings" + + # Get Col1 ID (parent ID) + col1_id = fresh_compositor.execute_lua(""" + local outputs = scroll.root_get_outputs() + local workspaces = scroll.output_get_workspaces(outputs[1]) + local ws1 + for i, ws in ipairs(workspaces) do + if scroll.workspace_get_name(ws) == "1" then + ws1 = ws + break + end + end + local tiling = scroll.workspace_get_tiling(ws1) + return tiling[1] + """) + print(f"Col1 ID: {col1_id}") + assert col1_id == w1_parent, "Col1 ID should match parent ID" + + # 4. Focus w1 to make it the focused_inactive_child of the parent + fresh_compositor.cmd(f"[con_id={w1_id}] focus") + focused = fresh_compositor.execute_lua("return scroll.focused_container()") + assert focused == w1_id, ( + f"w1 ({w1_id}) should be focused, but got {focused}" + ) + + # 5. Switch to workspace 2 + fresh_compositor.cmd("workspace 2") + + # 6. Kill w1 (which is on workspace 1) + fresh_compositor.cmd(f"[con_id={w1_id}] kill") + + # Wait for client1 to exit (destruction complete) + client1.wait(timeout=5) + time.sleep(0.1) + + # 7. Switch back to workspace 1. + fresh_compositor.cmd("workspace 1") + + # 8. Run move command targeted at Col1 (which has dangling focused_inactive_child) + # This should trigger UAF in container_get_active_view! + fresh_compositor.cmd(f"[con_id={col1_id}] move left nomode") + + # If we survive, let's verify w2 is still there + focused = fresh_compositor.execute_lua("return scroll.focused_container()") + print(f"Focused after move: {focused}") diff --git a/tests/test_fullscreen_move_uaf.py b/tests/test_fullscreen_move_uaf.py new file mode 100644 index 000000000..91737fb63 --- /dev/null +++ b/tests/test_fullscreen_move_uaf.py @@ -0,0 +1,48 @@ +from conftest import ScrollInstance +from test_utils import wayland_client, wait_for_client_map + + +def test_fullscreen_move_uaf(scroll_compositor: ScrollInstance) -> None: + # Start on workspace 1 + scroll_compositor.cmd("workspace 1") + + # Open a window + with wayland_client(scroll_compositor, "client1"): + wait_for_client_map(scroll_compositor, "client1") + + focused = scroll_compositor.execute_lua("return scroll.focused_container()") + print(f"Focused container: {focused}") + if focused: + print( + f"Focused type: {scroll_compositor.execute_lua(f'return scroll.node_get_type({focused})')}" + ) + print( + f"Is floating: {scroll_compositor.execute_lua(f'return scroll.container_get_floating({focused})')}" + ) + parent = scroll_compositor.execute_lua( + f"return scroll.container_get_parent({focused})" + ) + print(f"Parent: {parent}") + if parent: + print( + f"Parent type: {scroll_compositor.execute_lua(f'return scroll.node_get_type({parent})')}" + ) + + # Make it fullscreen + scroll_compositor.cmd("fullscreen") + + # Move it to workspace 2 + scroll_compositor.cmd("move container to workspace 2") + + # Now workspace 1's fullscreen pointer should be dangling + + # client1 is closed, container is destroyed. + # Workspace 1's fullscreen pointer is now pointing to freed memory. + + # Switch to workspace 1 to trigger arrange/focus logic which might access it + scroll_compositor.cmd("workspace 1") + + # Open another window on workspace 1 to force more layout activity + with wayland_client(scroll_compositor, "client2"): + wait_for_client_map(scroll_compositor, "client2") + scroll_compositor.cmd("nop") diff --git a/tests/test_leak_two_clients.py b/tests/test_leak_two_clients.py new file mode 100644 index 000000000..f50259359 --- /dev/null +++ b/tests/test_leak_two_clients.py @@ -0,0 +1,59 @@ +import time +import os +import subprocess +from pathlib import Path +from test_utils import wayland_client, wait_for_client_map, run_compositor + + +def test_leak_two_clients(scroll_compositor_binary: str, tmp_path: Path) -> None: + config_path = Path("./config.in").resolve() + config_content = config_path.read_text() + + # Prepare env with PATH including build dirs + project_root = Path(".").resolve() + build_dir = project_root / "build" + + old_path = os.environ.get("PATH", "") + build_paths = [ + str(build_dir / "sway"), + str(build_dir / "swaymsg"), + str(build_dir / "swaybar"), + str(build_dir / "swaynag"), + ] + os.environ["PATH"] = ":".join(build_paths) + ":" + old_path + + fresh_compositor = None + try: + with run_compositor(scroll_compositor_binary, tmp_path, config_content) as fc: + fresh_compositor = fc + # Start two clients and keep them running + with wayland_client(fresh_compositor, "client1"): + wait_for_client_map(fresh_compositor, "client1") + + with wayland_client(fresh_compositor, "client2"): + wait_for_client_map(fresh_compositor, "client2") + + # Let them run a bit + time.sleep(0.5) + + # Terminate compositor while clients are still running + fresh_compositor.proc.terminate() + try: + ret = fresh_compositor.proc.wait(timeout=5) + except subprocess.TimeoutExpired: + fresh_compositor.proc.kill() + ret = fresh_compositor.proc.wait() + + log_content = fresh_compositor.read_log() + print(f"Compositor log:\n{log_content}") + + assert ret == 0, f"Compositor exited with code {ret}" + except Exception as e: + if fresh_compositor: + try: + print(f"Compositor log on failure:\n{fresh_compositor.read_log()}") + except Exception as le: + print(f"Failed to read compositor log: {le}") + raise e + finally: + os.environ["PATH"] = old_path diff --git a/tests/test_lua_api.py b/tests/test_lua_api.py new file mode 100644 index 000000000..9d36fc62d --- /dev/null +++ b/tests/test_lua_api.py @@ -0,0 +1,177 @@ +from pathlib import Path +import time +from conftest import ScrollInstance +from test_utils import run_compositor + + +def test_lua_comprehensive_api(scroll_compositor: ScrollInstance) -> None: + # Get focused workspace + ws: int = scroll_compositor.execute_lua("return scroll.focused_workspace()") + assert isinstance(ws, int) + + # Get workspace name + ws_name: str = scroll_compositor.execute_lua( + f"return scroll.workspace_get_name({ws})" + ) + assert ws_name == "1" + + # Get workspace output + output: int = scroll_compositor.execute_lua( + f"return scroll.workspace_get_output({ws})" + ) + assert isinstance(output, int) + + # Get output name + output_name: str = scroll_compositor.execute_lua( + f"return scroll.output_get_name({output})" + ) + assert isinstance(output_name, str) + + # Get output enabled + output_enabled: bool = scroll_compositor.execute_lua( + f"return scroll.output_get_enabled({output})" + ) + assert output_enabled is True + + # Get root outputs + outputs: list = scroll_compositor.execute_lua("return scroll.root_get_outputs()") + assert isinstance(outputs, list) + assert len(outputs) > 0 + assert outputs[0] == output + + # Get output workspaces + ws_list: list = scroll_compositor.execute_lua( + f"return scroll.output_get_workspaces({output})" + ) + assert isinstance(ws_list, list) + assert len(ws_list) > 0 + assert ws_list[0] == ws + + # Get node types + ws_type: str = scroll_compositor.execute_lua(f"return scroll.node_get_type({ws})") + assert ws_type == "workspace" + + output_type: str = scroll_compositor.execute_lua( + f"return scroll.node_get_type({output})" + ) + assert output_type == "output" + + # Test invalid IDs + invalid_ws: int = 999999 + assert ( + scroll_compositor.execute_lua(f"return scroll.node_get_type({invalid_ws})") + is None + ) + assert ( + scroll_compositor.execute_lua(f"return scroll.workspace_get_name({invalid_ws})") + is None + ) + assert ( + scroll_compositor.execute_lua( + f"return scroll.workspace_get_output({invalid_ws})" + ) + is None + ) + + invalid_output: int = 999998 + assert ( + scroll_compositor.execute_lua( + f"return scroll.output_get_name({invalid_output})" + ) + is None + ) + assert ( + scroll_compositor.execute_lua( + f"return scroll.output_get_enabled({invalid_output})" + ) + is False + ) + + invalid_output_ws: list = scroll_compositor.execute_lua( + f"return scroll.output_get_workspaces({invalid_output})" + ) + assert isinstance(invalid_output_ws, list) + assert len(invalid_output_ws) == 0 + + assert scroll_compositor.proc.poll() is None + + +def test_lua_tilde_expansion(scroll_compositor: ScrollInstance) -> None: + home_dir = scroll_compositor.temp_dir + script_path = home_dir / "test_tilde.lua" + script_path.write_text('scroll.log("TILDE_TEST_SUCCESS")') + + with scroll_compositor.assert_logs_match("TILDE_TEST_SUCCESS"): + res = scroll_compositor.cmd("lua ~/test_tilde.lua") + assert res[0]["success"] + + # Test non-existent file + res = scroll_compositor.cmd("lua ~/nonexistent.lua") + assert not res[0]["success"] + assert "Error" in res[0].get("error", "") + + # Test multiple matches (should fail) + script_path2 = home_dir / "test_tilde2.lua" + script_path2.write_text('scroll.log("TILDE_TEST_SUCCESS2")') + + res = scroll_compositor.cmd("lua ~/test_tilde*.lua") + assert not res[0]["success"] + assert "multiple files" in res[0].get("error", "") + + +def test_lua_relative_path_config_load( + scroll_compositor: ScrollInstance, tmp_path: Path +) -> None: + binary_path = scroll_compositor.proc.args[0] + script_path = tmp_path / "test_relative.lua" + script_path.write_text('scroll.log("RELATIVE_LOAD_SUCCESS")') + + config = "workspace 1\nxwayland force\nlua test_relative.lua\n" + with run_compositor(binary_path, tmp_path, config) as inst: + time.sleep(1.0) + assert "RELATIVE_LOAD_SUCCESS" in inst.read_log() + + +def test_lua_relative_path_subdir_config_load( + scroll_compositor: ScrollInstance, tmp_path: Path +) -> None: + binary_path = scroll_compositor.proc.args[0] + subdir = tmp_path / "scripts" + subdir.mkdir() + script_path = subdir / "test_relative2.lua" + script_path.write_text('scroll.log("RELATIVE_SUBDIR_LOAD_SUCCESS")') + + config = "workspace 1\nxwayland force\nlua scripts/test_relative2.lua\n" + with run_compositor(binary_path, tmp_path, config) as inst: + time.sleep(1.0) + assert "RELATIVE_SUBDIR_LOAD_SUCCESS" in inst.read_log() + + +def test_lua_relative_glob_config_load( + scroll_compositor: ScrollInstance, tmp_path: Path +) -> None: + binary_path = scroll_compositor.proc.args[0] + subdir = tmp_path / "scripts" + subdir.mkdir() + script_path = subdir / "test_glob1.lua" + script_path.write_text('scroll.log("RELATIVE_GLOB_LOAD_SUCCESS")') + + config = "workspace 1\nxwayland force\nlua scripts/test_glob*.lua\n" + with run_compositor(binary_path, tmp_path, config) as inst: + time.sleep(1.0) + assert "RELATIVE_GLOB_LOAD_SUCCESS" in inst.read_log() + + +def test_lua_relative_glob_multiple_config_load( + scroll_compositor: ScrollInstance, tmp_path: Path +) -> None: + binary_path = scroll_compositor.proc.args[0] + subdir = tmp_path / "scripts" + subdir.mkdir() + (subdir / "test_glob1.lua").write_text('scroll.log("G1")') + (subdir / "test_glob2.lua").write_text('scroll.log("G2")') + + config = "workspace 1\nxwayland force\nlua scripts/test_glob*.lua\n" + with run_compositor(binary_path, tmp_path, config) as inst: + time.sleep(1.0) + assert "Path expanded to multiple files" in inst.read_log() diff --git a/tests/test_lua_error.py b/tests/test_lua_error.py new file mode 100644 index 000000000..d29903efd --- /dev/null +++ b/tests/test_lua_error.py @@ -0,0 +1,241 @@ +import uuid +from contextlib import contextmanager +from pathlib import Path +from typing import Generator +from conftest import ScrollInstance +from test_utils import wayland_client, wait_for_client_map + + +@contextmanager +def lua_callback( + scroll_compositor: ScrollInstance, event_name: str, callback_code: str +) -> Generator[str, None, None]: + cb_key: str = "cb_" + uuid.uuid4().hex + + register_code: str = f""" + if not _G.test_callbacks then _G.test_callbacks = {{}} end + _G.test_callbacks["{cb_key}"] = scroll.add_callback("{event_name}", {callback_code}, nil) + return "{cb_key}" + """ + res = scroll_compositor.execute_lua(register_code) + assert res == cb_key + + try: + yield cb_key + finally: + unregister_code: str = f""" + if _G.test_callbacks and _G.test_callbacks["{cb_key}"] then + scroll.remove_callback(_G.test_callbacks["{cb_key}"]) + _G.test_callbacks["{cb_key}"] = nil + end + """ + scroll_compositor.execute_lua(unregister_code) + + +def test_workspace_focus_error(scroll_compositor: ScrollInstance) -> None: + marker: str = "ERR_WORKSPACE_FOCUS" + with lua_callback( + scroll_compositor, + "workspace_focus", + f"function(ws, data) error('{marker}') end", + ): + with scroll_compositor.assert_logs_match(rf"Lua error:.*{marker}"): + scroll_compositor.cmd("workspace 2") + scroll_compositor.cmd("workspace 1") + + assert scroll_compositor.proc.poll() is None + + +def test_workspace_create_error(scroll_compositor: ScrollInstance) -> None: + marker: str = "ERR_WORKSPACE_CREATE" + import random + + ws_num: int = random.randint(10, 100) + with lua_callback( + scroll_compositor, + "workspace_create", + f"function(ws, data) error('{marker}') end", + ): + with scroll_compositor.assert_logs_match(rf"Lua error:.*{marker}"): + scroll_compositor.cmd(f"workspace {ws_num}") + scroll_compositor.cmd("workspace 1") + + assert scroll_compositor.proc.poll() is None + + +def test_view_map_error(scroll_compositor: ScrollInstance) -> None: + marker: str = "ERR_VIEW_MAP" + with lua_callback( + scroll_compositor, + "view_map", + f"function(con, data) error('{marker}') end", + ): + with scroll_compositor.assert_logs_match(rf"Lua error:.*{marker}"): + with wayland_client(scroll_compositor, "Test Window"): + wait_for_client_map(scroll_compositor, "Test Window") + + assert scroll_compositor.proc.poll() is None + + +def test_view_unmap_error(scroll_compositor: ScrollInstance) -> None: + marker: str = "ERR_VIEW_UNMAP" + with wayland_client(scroll_compositor, "Test Window") as proc: + wait_for_client_map(scroll_compositor, "Test Window") + with lua_callback( + scroll_compositor, + "view_unmap", + f"function(con, data) error('{marker}') end", + ): + with scroll_compositor.assert_logs_match(rf"Lua error:.*{marker}"): + proc.terminate() + proc.wait(timeout=5) + + assert scroll_compositor.proc.poll() is None + + +def test_view_focus_error(scroll_compositor: ScrollInstance) -> None: + marker: str = "ERR_VIEW_FOCUS" + with wayland_client(scroll_compositor, "Test Window"): + wait_for_client_map(scroll_compositor, "Test Window") + scroll_compositor.cmd("workspace 2") + + with lua_callback( + scroll_compositor, + "view_focus", + f"function(con, data) error('{marker}') end", + ): + with scroll_compositor.assert_logs_match(rf"Lua error:.*{marker}"): + scroll_compositor.cmd("workspace 1") + + assert scroll_compositor.proc.poll() is None + + +def test_view_float_error(scroll_compositor: ScrollInstance) -> None: + marker: str = "ERR_VIEW_FLOAT" + with wayland_client(scroll_compositor, "Test Window"): + wait_for_client_map(scroll_compositor, "Test Window") + with lua_callback( + scroll_compositor, + "view_float", + f"function(con, data) error('{marker}') end", + ): + with scroll_compositor.assert_logs_match(rf"Lua error:.*{marker}"): + scroll_compositor.cmd("floating toggle") + + assert scroll_compositor.proc.poll() is None + + +def test_view_urgent_error(scroll_compositor: ScrollInstance) -> None: + marker: str = "ERR_VIEW_URGENT" + with wayland_client(scroll_compositor, "Test Window"): + view_id = wait_for_client_map(scroll_compositor, "Test Window") + # Switch focus away so the view is not focused (urgent cannot be set on focused view) + scroll_compositor.cmd("workspace 2") + try: + with lua_callback( + scroll_compositor, + "view_urgent", + f"function(con, data) error('{marker}') end", + ): + with scroll_compositor.assert_logs_match(rf"Lua error:.*{marker}"): + scroll_compositor.execute_lua( + f"scroll.view_set_urgent({view_id}, true)" + ) + finally: + scroll_compositor.cmd("workspace 1") + + assert scroll_compositor.proc.poll() is None + + +def test_ipc_workspace_error(scroll_compositor: ScrollInstance) -> None: + marker: str = "ERR_IPC_WORKSPACE" + with lua_callback( + scroll_compositor, + "ipc_workspace", + f"function(old, new, change, data) error('{marker}') end", + ): + with scroll_compositor.assert_logs_match(rf"Lua error:.*{marker}"): + scroll_compositor.cmd("workspace 2") + scroll_compositor.cmd("workspace 1") + + assert scroll_compositor.proc.poll() is None + + +def test_ipc_view_error(scroll_compositor: ScrollInstance) -> None: + marker: str = "ERR_IPC_VIEW" + with lua_callback( + scroll_compositor, + "ipc_view", + f"function(con, change, data) error('{marker}') end", + ): + with scroll_compositor.assert_logs_match(rf"Lua error:.*{marker}"): + with wayland_client(scroll_compositor, "Test Window"): + wait_for_client_map(scroll_compositor, "Test Window") + + assert scroll_compositor.proc.poll() is None + + +def test_command_end_error(scroll_compositor: ScrollInstance) -> None: + marker: str = "ERR_COMMAND_END" + with lua_callback( + scroll_compositor, + "command_end", + f"function(cmd, data) error('{marker}') end", + ): + with scroll_compositor.assert_logs_match(rf"Lua error:.*{marker}"): + scroll_compositor.execute_lua('scroll.command(nil, "nop")') + + assert scroll_compositor.proc.poll() is None + + +def test_lua_load_nonexistent_script(scroll_compositor: ScrollInstance) -> None: + res = scroll_compositor.cmd("lua /nonexistent/script.lua") + assert res and not res[0]["success"] + assert "Error" in res[0]["error"] + assert scroll_compositor.proc.poll() is None + + +def test_lua_load_syntax_error( + scroll_compositor: ScrollInstance, tmp_path: Path +) -> None: + script: Path = tmp_path / "syntax_error.lua" + script.write_text("this is not valid lua code") + res = scroll_compositor.cmd(f"lua {script}") + assert res and not res[0]["success"] + assert "Error" in res[0]["error"] + assert scroll_compositor.proc.poll() is None + + +def test_lua_runtime_error_top_level( + scroll_compositor: ScrollInstance, tmp_path: Path +) -> None: + script: Path = tmp_path / "runtime_error.lua" + script.write_text("error('top level error')") + res = scroll_compositor.cmd(f"lua {script}") + assert res and not res[0]["success"] + assert "Error" in res[0]["error"] + assert "top level error" in res[0]["error"] + assert scroll_compositor.proc.poll() is None + + +def test_jump_end_error(scroll_compositor: ScrollInstance) -> None: + marker: str = "ERR_JUMP_END" + with wayland_client(scroll_compositor, "Test Window"): + wait_for_client_map(scroll_compositor, "Test Window") + with lua_callback( + scroll_compositor, + "jump_end", + f"function(con, data) error('{marker}') end", + ): + # Enter jump mode + res = scroll_compositor.cmd("jump") + assert res and res[0]["success"] + + # Trigger end by clicking + with scroll_compositor.assert_logs_match(rf"Lua error:.*{marker}"): + r1 = scroll_compositor.cmd("seat * cursor press button1") + assert r1 and r1[0]["success"], f"cursor press failed: {r1}" + r2 = scroll_compositor.cmd("seat * cursor release button1") + assert r2 and r2[0]["success"], f"cursor release failed: {r2}" + + assert scroll_compositor.proc.poll() is None diff --git a/tests/test_lua_safety.py b/tests/test_lua_safety.py new file mode 100644 index 000000000..7911868a8 --- /dev/null +++ b/tests/test_lua_safety.py @@ -0,0 +1,27 @@ +from conftest import ScrollInstance + + +def test_lua_use_after_free_prevented(scroll_compositor: ScrollInstance) -> None: + # 1. Get current workspace (workspace 1) + ws1: int = scroll_compositor.execute_lua("return scroll.focused_workspace()") + + # 2. Create and switch to workspace 2 + scroll_compositor.execute_lua('scroll.command(nil, "workspace 2")') + ws2: int = scroll_compositor.execute_lua("return scroll.focused_workspace()") + assert ws1 != ws2 + + # 3. Switch back to workspace 1 (workspace 2 should be destroyed if empty) + scroll_compositor.execute_lua('scroll.command(nil, "workspace 1")') + + # 4. Now try to get name of ws2. It should return nil (None in Python), but NOT crash. + ws2_name = scroll_compositor.execute_lua(f"return scroll.workspace_get_name({ws2})") + assert ws2_name is None + + # 5. Also try with invalid container ID + invalid_con_title = scroll_compositor.execute_lua( + "return scroll.view_get_title(999999)" + ) + assert invalid_con_title is None + + # Verify it didn't crash + assert scroll_compositor.proc.poll() is None diff --git a/tests/test_move_cleanup_crash.py b/tests/test_move_cleanup_crash.py new file mode 100644 index 000000000..e7d9949ed --- /dev/null +++ b/tests/test_move_cleanup_crash.py @@ -0,0 +1,42 @@ +import time +from conftest import ScrollInstance +from test_utils import wayland_client, wait_for_client_map + + +def test_move_cleanup_uaf_crash(fresh_compositor: ScrollInstance) -> None: + # 1. Open Window 1 on Workspace 1 + with wayland_client(fresh_compositor, "Window 1"): + wait_for_client_map(fresh_compositor, "Window 1") + + # Make Window 1 floating + res = fresh_compositor.cmd("floating enable") + assert res and res[0]["success"], f"floating enable failed: {res}" + + # 2. Switch to Workspace 3 (so Workspace 1 becomes inactive) + res = fresh_compositor.cmd("workspace 3") + assert res and res[0]["success"], f"workspace 3 failed: {res}" + + time.sleep(0.5) + + # 3. Use criteria to move Window 1 from Workspace 1 to Workspace 2. + # This should make Workspace 1 empty and inactive, so it gets destroyed. + # Then the move command cleanup code should UAF on Workspace 1. + try: + res = fresh_compositor.cmd( + '[title="Window 1"] move container to workspace 2' + ) + assert res and res[0]["success"], f"move failed: {res}" + except Exception as e: + print(f"Compositor log:\n{fresh_compositor.read_log()}") + raise e + + # Check if compositor process is still alive + log_content = fresh_compositor.read_log() + print(f"Compositor log:\n{log_content}") + if ( + fresh_compositor.proc.poll() is not None + or "node_table not initialized" in log_content + ): + pass + + assert fresh_compositor.proc.poll() is None, "Compositor crashed" diff --git a/tests/test_normal_exit.py b/tests/test_normal_exit.py new file mode 100644 index 000000000..0a51e8e7b --- /dev/null +++ b/tests/test_normal_exit.py @@ -0,0 +1,34 @@ +import time +from conftest import ScrollInstance + + +def test_normal_exit_no_errors(fresh_compositor: ScrollInstance) -> None: + # Send exit command + try: + res = fresh_compositor.cmd("exit") + assert res and res[0]["success"], f"Exit command failed: {res}" + except EOFError: + # Expected if compositor exits before flushing reply + pass + except Exception as e: + print(f"Compositor log:\n{fresh_compositor.read_log()}") + raise e + + # Wait for compositor to exit + tries = 0 + poll = None + while tries < 50: + poll = fresh_compositor.proc.poll() + if poll is not None: + break + time.sleep(0.1) + tries += 1 + + assert poll is not None, "Compositor did not exit" + + log_content = fresh_compositor.read_log() + if poll != 0 or "node_table not initialized" in log_content: + print(f"Compositor log:\n{log_content}") + + assert poll == 0, f"Compositor exited with non-zero code: {poll}" + assert "node_table not initialized" not in log_content diff --git a/tests/test_scratchpad_crash.py b/tests/test_scratchpad_crash.py new file mode 100644 index 000000000..0f3b060a3 --- /dev/null +++ b/tests/test_scratchpad_crash.py @@ -0,0 +1,51 @@ +from conftest import ScrollInstance +from test_utils import wayland_client, wait_for_client_map + + +def test_scratchpad_double_reap_crash(fresh_compositor: ScrollInstance) -> None: + # 1. Open Window 1 + with wayland_client(fresh_compositor, "Window 1"): + wait_for_client_map(fresh_compositor, "Window 1") + + # 2. Set mode to vertical to stack next window + res = fresh_compositor.cmd("set_mode v") + assert res and res[0]["success"], f"set_mode v failed: {res}" + + # 3. Open Window 2 + with wayland_client(fresh_compositor, "Window 2"): + wait_for_client_map(fresh_compositor, "Window 2") + + # Now we have: Workspace -> Split (V) -> [Window 1, Window 2] + + # 4. Send Window 2 to scratchpad + res = fresh_compositor.cmd("move scratchpad") + assert res and res[0]["success"], f"move scratchpad 2 failed: {res}" + + # V-split still has Window 1. + + # 5. Focus Window 1 (it should be focused automatically after 2 is sent) + view_id = fresh_compositor.execute_lua("return scroll.focused_view()") + title = fresh_compositor.execute_lua( + f"return scroll.view_get_title({view_id})" + ) + assert title == "Window 1", f"Window 1 not focused, got {title}" + + # 6. Send Window 1 to scratchpad + # This should make V-split empty, trigger reap in set_floating, + # and then UAF/crash in root_scratchpad_add_container. + try: + res = fresh_compositor.cmd("move scratchpad") + assert res and res[0]["success"], f"move scratchpad 1 failed: {res}" + except Exception as e: + print(f"Compositor log:\n{fresh_compositor.read_log()}") + raise e + + # Check if compositor process is still alive + log_content = fresh_compositor.read_log() + if ( + fresh_compositor.proc.poll() is not None + or "node_table not initialized" in log_content + ): + print(f"Compositor log:\n{log_content}") + + assert fresh_compositor.proc.poll() is None, "Compositor crashed" diff --git a/tests/test_space_aba.py b/tests/test_space_aba.py new file mode 100644 index 000000000..c01e83e30 --- /dev/null +++ b/tests/test_space_aba.py @@ -0,0 +1,41 @@ +import time +from conftest import ScrollInstance +from test_utils import wayland_client, wait_for_client_map + + +def test_space_aba(fresh_compositor: ScrollInstance) -> None: + # 1. Create client1 on WS 1 + with wayland_client(fresh_compositor, "client1"): + wait_for_client_map(fresh_compositor, "client1") + + # Save space "sp1" + fresh_compositor.cmd("space_save sp1") + + # client1 is closed now. + # Wait for it to be fully destroyed. + time.sleep(0.2) + + # 2. Create client2 on WS 1. + # Hopefully it reuses client1's view struct address. + with wayland_client(fresh_compositor, "client2"): + wait_for_client_map(fresh_compositor, "client2") + + # Switch to WS 2 + fresh_compositor.cmd("workspace 2") + + # Load space "sp1" on WS 2. + # If ABA bug occurs, it might find client2 (matching old client1 address) + # and move it to WS 2. + fresh_compositor.cmd("space_load sp1 load") + + # Check if client2 is visible on WS 2. + # If it was moved to WS 2, it should be focused because space_load focuses restored containers. + focused_title = fresh_compositor.execute_lua(""" + local view = scroll.focused_view() + return view and scroll.view_get_title(view) + """) + print(f"Focused title after space_load: {focused_title}") + + assert focused_title != "client2", ( + "ABA bug: client2 was incorrectly moved to WS 2!" + ) diff --git a/tests/test_space_crash.py b/tests/test_space_crash.py new file mode 100644 index 000000000..6e35559e6 --- /dev/null +++ b/tests/test_space_crash.py @@ -0,0 +1,83 @@ +from conftest import ScrollInstance +from test_utils import wayland_client, wait_for_client_map + + +def test_space_restore_uaf_crash(fresh_compositor: ScrollInstance) -> None: + # 1. Open Window 1 on Workspace 1 + with wayland_client(fresh_compositor, "Window 1"): + wait_for_client_map(fresh_compositor, "Window 1") + + # Make Window 1 floating + res = fresh_compositor.cmd("floating enable") + assert res and res[0]["success"], f"floating enable failed: {res}" + + # Save layout "space1" on Workspace 1 + res = fresh_compositor.cmd("space save space1") + assert res and res[0]["success"], f"space save failed: {res}" + + # 2. Switch to Workspace 2 + res = fresh_compositor.cmd("workspace 2") + assert res and res[0]["success"], f"workspace 2 failed: {res}" + + # Move Window 1 to Workspace 2 + # (It should still be floating? Yes, moving floating window to workspace works) + # Actually, we can just move it. + # Wait, if we are on Workspace 2, we can't easily move it here unless we focus it. + # But we switched to Workspace 2, so focus is on Workspace 2 (empty). + # We should go back to Workspace 1, move it to Workspace 2, then go to Workspace 2. + res = fresh_compositor.cmd("workspace 1") + assert res and res[0]["success"], f"workspace 1 failed: {res}" + + res = fresh_compositor.cmd("move container to workspace 2") + assert res and res[0]["success"], f"move to ws 2 failed: {res}" + + res = fresh_compositor.cmd("workspace 2") + assert res and res[0]["success"], f"workspace 2 failed: {res}" + + # Now Window 1 is floating on Workspace 2. + # We must make it tiled on Workspace 2. + res = fresh_compositor.cmd("floating disable") + assert res and res[0]["success"], f"floating disable failed: {res}" + + # Set mode to vertical to stack next window + res = fresh_compositor.cmd("set_mode v") + assert res and res[0]["success"], f"set_mode v failed: {res}" + + # Open Window 2 on Workspace 2 + with wayland_client(fresh_compositor, "Window 2"): + wait_for_client_map(fresh_compositor, "Window 2") + + # Now we have on Workspace 2: Split (V) -> [Window 1, Window 2] + + # Move Window 2 to Workspace 3 (so Window 1 is only child of V-split) + res = fresh_compositor.cmd("move container to workspace 3") + assert res and res[0]["success"], f"move to ws 3 failed: {res}" + + # Now Window 1 is the only child of V-split on Workspace 2. + # Workspace 2 has no other windows. + + # 3. Go to Workspace 1 + res = fresh_compositor.cmd("workspace 1") + assert res and res[0]["success"], f"workspace 1 failed: {res}" + + # 4. Restore layout "space1" + # This should try to restore Window 1 as floating on Workspace 1. + # It will detach Window 1 from Workspace 2, reaping V-split and destroying Workspace 2. + # Then it will call arrange_container with dangling Workspace 2 pointer. + try: + res = fresh_compositor.cmd("space restore space1") + assert res and res[0]["success"], f"space restore failed: {res}" + except Exception as e: + print(f"Compositor log:\n{fresh_compositor.read_log()}") + raise e + + # Check if compositor process is still alive + log_content = fresh_compositor.read_log() + print(f"Compositor log:\n{log_content}") + if ( + fresh_compositor.proc.poll() is not None + or "node_table not initialized" in log_content + ): + pass + + assert fresh_compositor.proc.poll() is None, "Compositor crashed" diff --git a/tests/test_swap.py b/tests/test_swap.py new file mode 100644 index 000000000..d409024f2 --- /dev/null +++ b/tests/test_swap.py @@ -0,0 +1,13 @@ +from conftest import ScrollInstance + + +def test_swap_invalid_con_id(scroll_compositor: ScrollInstance) -> None: + # Try swap with invalid con_id format + res: list = scroll_compositor.cmd("swap container with con_id invalid123") + assert not res[0]["success"] + assert "Invalid container ID" in res[0].get("error", "") + + # Try swap with valid con_id format but non-existent + res = scroll_compositor.cmd("swap container with con_id 999999") + assert not res[0]["success"] + assert "Failed to find con_id '999999'" in res[0].get("error", "") diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 000000000..3515d980f --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,281 @@ +import time +import os +import subprocess +import re +import pytest +from typing import Generator, Any +from contextlib import contextmanager +from pathlib import Path +from scrollipc import ScrollIPC + +RUNNER_LUA_CONTENT: str = """ +local args = ... +local output_path = args[1] +local user_code_path = args[2] + +local function escape_str(s) + return '"' .. s:gsub('\\\\', '\\\\\\\\'):gsub('"', '\\\\"'):gsub('\\n', '\\\\n'):gsub('\\r', '\\\\r'):gsub('\\t', '\\\\t') .. '"' +end + +local function serialize(val) + if val == nil then return "null" end + if type(val) == "boolean" then return val and "true" or "false" end + if type(val) == "number" then return tostring(val) end + if type(val) == "string" then return escape_str(val) end + if type(val) == "table" then + local is_list = true + local max_idx = 0 + local count = 0 + for k, v in pairs(val) do + count = count + 1 + if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then + is_list = false + break + end + if k > max_idx then max_idx = k end + end + if is_list and max_idx == count then + local parts = {} + for i = 1, max_idx do + table.insert(parts, serialize(val[i])) + end + return "[" .. table.concat(parts, ",") .. "]" + else + local parts = {} + for k, v in pairs(val) do + if type(k) == "string" then + table.insert(parts, escape_str(k) .. ":" .. serialize(v)) + end + end + return "{" .. table.concat(parts, ",") .. "}" + end + end + return "null" +end + +local chunk, err = loadfile(user_code_path) +local success, result +local results +if chunk then + results = { pcall(chunk) } + success = results[1] +else + success = false + results = { false, "Error loading code: " .. tostring(err) } +end + +local f = io.open(output_path, "w") +if success then + f:write("SUCCESS\\n") + if #results <= 1 then + f:write("null") + elseif #results == 2 then + f:write(serialize(results[2])) + else + local parts = {} + for i = 2, #results do + table.insert(parts, serialize(results[i])) + end + f:write("[" .. table.concat(parts, ",") .. "]") + end +else + f:write("ERROR\\n") + f:write(tostring(results[2])) +end +f:close() +""" + + +class ScrollInstance: + proc: subprocess.Popen + ipc: ScrollIPC + log_path: Path + temp_dir: Path + + def __init__( + self, proc: subprocess.Popen, ipc: ScrollIPC, log_path: Path, temp_dir: Path + ): + self.proc = proc + self.ipc = ipc + self.log_path = log_path + self.temp_dir = temp_dir + + def cmd(self, command: str) -> list: + return self.ipc.command(command) + + def read_log(self) -> str: + return self.log_path.read_text() + + def execute_lua(self, code: str) -> Any: + import json + + runner_path = self.temp_dir / "exec_runner.lua" + if not runner_path.exists(): + runner_path.write_text(RUNNER_LUA_CONTENT) + + if not hasattr(self, "_lua_execute_counter"): + self._lua_execute_counter = 0 + counter = self._lua_execute_counter + self._lua_execute_counter += 1 + + user_code_path = self.temp_dir / f"user_code_{counter}.lua" + output_path = self.temp_dir / f"exec_{counter}.out" + + user_code_path.write_text(code) + + res = self.cmd(f"lua {runner_path} {output_path} {user_code_path}") + assert res[0]["success"], f"Failed to run lua command: {res}" + + assert output_path.exists(), ( + f"Output file not created: {output_path}. Compositor log:\\n{self.read_log()}" + ) + output_content = output_path.read_text() + + lines = output_content.splitlines() + if not lines: + raise RuntimeError( + f"Lua runner output is empty. Compositor log:\\n{self.read_log()}" + ) + status = lines[0] + result_str = "\\n".join(lines[1:]) + + if status == "SUCCESS": + if not result_str: + return None + return json.loads(result_str) + else: + raise RuntimeError(f"Lua execution failed: {result_str}") + + def getenv(self, var: str) -> str | None: + return self.execute_lua(f'return os.getenv("{var}")') + + @contextmanager + def assert_logs_match( + self, pattern: str, timeout: float = 5.0 + ) -> Generator[None, None, None]: + initial_log_len: int = len(self.read_log()) + yield + start_time: float = time.time() + compiled_pattern = re.compile(pattern) + while True: + current_log: str = self.read_log() + new_log: str = current_log[initial_log_len:] + if compiled_pattern.search(new_log): + return + if time.time() - start_time > timeout: + raise AssertionError( + f"Pattern '{pattern}' not found in new log output within {timeout}s.\nNew log was:\n{new_log}" + ) + time.sleep(0.1) + + +@contextmanager +def run_compositor( + binary_path: str, temp_dir: Path, config_content: str | None = None +) -> Generator[ScrollInstance, None, None]: + log_path: Path = temp_dir / "scroll.log" + log_file = open(log_path, "w") + + config_path: Path = temp_dir / "config" + if config_content is None: + config_content = "workspace 1\nxwayland force\n" + config_path.write_text(config_content) + + env = os.environ.copy() + env["HOME"] = str(temp_dir) + env["WLR_BACKENDS"] = "headless" + + tests_dir = Path(__file__).parent.resolve() + supp_path = tests_dir / "lsan.supp" + if "LSAN_OPTIONS" in env: + env["LSAN_OPTIONS"] = f"suppressions={supp_path}:{env['LSAN_OPTIONS']}" + else: + env["LSAN_OPTIONS"] = f"suppressions={supp_path}" + if "DISPLAY" in env: + del env["DISPLAY"] + if "WAYLAND_DISPLAY" in env: + del env["WAYLAND_DISPLAY"] + + proc = subprocess.Popen( + [binary_path, "-c", str(config_path), "-d"], + env=env, + stdout=log_file, + stderr=subprocess.STDOUT, + ) + + xdg_runtime_dir: str = os.environ.get("XDG_RUNTIME_DIR", "/tmp") + uid: int = os.getuid() + socket_path: str = os.path.join( + xdg_runtime_dir, f"scroll-ipc.{uid}.{proc.pid}.sock" + ) + + ipc = None + tries = 0 + while tries < 100: + if os.path.exists(socket_path): + try: + ipc = ScrollIPC(socket_path) + break + except Exception: + pass + time.sleep(0.05) + tries += 1 + + if not ipc: + proc.terminate() + log_file.close() + print(f"Scroll log:\n{log_path.read_text()}") + pytest.exit("Failed to connect to scroll IPC") + + try: + yield ScrollInstance(proc, ipc, log_path, temp_dir) + finally: + # Teardown + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + log_file.close() + + +@contextmanager +def wayland_client( + compositor: ScrollInstance, + title: str, +) -> Generator[subprocess.Popen, None, None]: + wayland_display: str | None = compositor.getenv("WAYLAND_DISPLAY") + assert wayland_display is not None + client_path: Path = Path("./build/tests/wayland-test-client").resolve() + assert client_path.exists(), f"Client not found at {client_path}" + env: dict = os.environ.copy() + env["WAYLAND_DISPLAY"] = wayland_display + proc: subprocess.Popen = subprocess.Popen( + [str(client_path), title, "test_app"], env=env + ) + try: + yield proc + finally: + if proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + + +def wait_for_client_map(compositor: ScrollInstance, title: str) -> int: + tries: int = 0 + while tries < 50: + view_id = compositor.execute_lua(f""" + local view = scroll.focused_view() + if view and scroll.view_get_title(view) == "{title}" then + return view + end + """) + if view_id is not None: + assert isinstance(view_id, int) + return view_id + time.sleep(0.05) + tries += 1 + raise RuntimeError(f"Client '{title}' did not map") diff --git a/tests/test_workspace_animation_uaf.py b/tests/test_workspace_animation_uaf.py new file mode 100644 index 000000000..3808faafa --- /dev/null +++ b/tests/test_workspace_animation_uaf.py @@ -0,0 +1,50 @@ +import time +from typing import Generator +from pathlib import Path +import pytest +from conftest import ScrollInstance +from test_utils import wayland_client, wait_for_client_map, run_compositor + + +@pytest.fixture(scope="function") +def animating_compositor( + scroll_compositor_binary: str, tmp_path: Path +) -> Generator[ScrollInstance, None, None]: + config: str = ( + "workspace 1\n" + "xwayland force\n" + "animations enabled yes\n" + "animations workspace_switch yes 5000\n" + ) + with run_compositor(scroll_compositor_binary, tmp_path, config) as inst: + yield inst + + +def test_workspace_switch_active_uaf(animating_compositor: ScrollInstance) -> None: + # 1. Create w1 on Workspace 1 + with wayland_client(animating_compositor, "client1") as client1: + wait_for_client_map(animating_compositor, "client1") + w1_id = animating_compositor.execute_lua("return scroll.focused_container()") + print(f"w1_id: {w1_id}") + + # 2. Switch to Workspace 2. + # This starts a 5s animation. WS 1 is NOT empty, so data->from = WS 1. + animating_compositor.cmd("workspace 2") + + # Sleep a bit to ensure animation is ongoing and IPC commands are sequenced + time.sleep(0.1) + + # 3. Kill w1 (on WS 1) during animation. + # WS 1 should become empty and be destroyed/freed. + animating_compositor.cmd(f"[con_id={w1_id}] kill") + + # Wait for client1 to exit + client1.wait(timeout=5) + + # Sleep a bit to let transaction destroy run in idle loop + time.sleep(0.1) + + # 4. Switch back to Workspace 1. + # This will cancel the ongoing animation, triggering workspace_switch_callback_end. + # It should crash here if WS 1 was freed! + animating_compositor.cmd("workspace 1") diff --git a/tests/test_workspace_split_uaf.py b/tests/test_workspace_split_uaf.py new file mode 100644 index 000000000..cb0345171 --- /dev/null +++ b/tests/test_workspace_split_uaf.py @@ -0,0 +1,59 @@ +import time +from conftest import ScrollInstance +from test_utils import wayland_client, wait_for_client_map + + +def test_workspace_split_uaf_crash(fresh_compositor: ScrollInstance) -> None: + try: + # 1. Open Window 1 on Workspace 1 (active on HEADLESS-1) + with wayland_client(fresh_compositor, "Window 1"): + wait_for_client_map(fresh_compositor, "Window 1") + + # 2. Split workspace 1. This creates workspace 2 as sibling. + # Workspace 1 has Window 1, Workspace 2 is empty. + res = fresh_compositor.cmd("workspace split") + assert res and res[0]["success"], f"workspace split failed: {res}" + + # 3. Create a second output HEADLESS-2. + # It should get a default workspace (probably 3). + res = fresh_compositor.cmd("create_output") + assert res and res[0]["success"], f"create_output failed: {res}" + + time.sleep(0.5) + + # 4. Unplug HEADLESS-1. + # Workspaces 1 and 2 should be evacuated. + # Workspace 1 (non-empty) is moved to HEADLESS-2. + # Workspace 2 (empty) is destroyed. + res = fresh_compositor.cmd("output HEADLESS-1 unplug") + assert res and res[0]["success"], f"unplug failed: {res}" + + time.sleep(0.5) + + # 5. Focus Workspace 3 on HEADLESS-2 (so Workspace 1 becomes inactive) + res = fresh_compositor.cmd("workspace 3") + assert res and res[0]["success"], f"workspace 3 failed: {res}" + + time.sleep(0.5) + + # 6. Move Window 1 from Workspace 1 to Workspace 3. + # Since Window 1 is moved out of Workspace 1, and Workspace 1 is inactive, + # it should trigger workspace_consider_destroy(Workspace 1). + # Workspace 1 is empty, and it is split (sibling was Workspace 2). + # It will try to access Workspace 2 (which is destroyed) -> UAF. + res = fresh_compositor.cmd( + '[title="Window 1"] move container to workspace 3' + ) + assert res and res[0]["success"], f"move container failed: {res}" + + # Check if compositor process is still alive + log_content = fresh_compositor.read_log() + print(f"Compositor log:\n{log_content}") + assert fresh_compositor.proc.poll() is None, "Compositor crashed" + except Exception as e: + print(f"Test failed with exception: {e}") + try: + print(f"Compositor log:\n{fresh_compositor.read_log()}") + except Exception as log_err: + print(f"Failed to read compositor log: {log_err}") + raise e