ibus: cover context ownership through the engine

This commit is contained in:
2026-08-13 23:21:12 +09:00
parent bb2a970db1
commit 9f4f9ceb14
5 changed files with 379 additions and 68 deletions

52
ibus.c
View File

@@ -316,6 +316,38 @@ dropcontext(Ictx *ctx)
memset(ctx, 0, sizeof *ctx); memset(ctx, 0, sizeof *ctx);
} }
static void
dropconncontexts(DBusConnection *conn)
{
int i;
for(i = 0; i < nelem(contexts); i++)
if(contexts[i].conn == conn)
dropcontext(&contexts[i]);
}
static int
processkey(Ictx *ctx, u32int sym, u32int state, Keyres *res)
{
if(state & Relmask || !ctx->focused)
return 0;
sendrequest(ctx, Keypress, kget(sym), mget(state), res);
return 1;
}
static void
setcursor(Ictx *ctx, int x, int y, int h)
{
Keyres res;
ctx->caret.valid = 1;
ctx->caret.x = x;
ctx->caret.y = y;
ctx->caret.h = h;
if(ctx->focused)
sendrequest(ctx, Keycaret, 0, 0, &res);
}
static void static void
appendibustext(DBusMessageIter *it, const char *s) appendibustext(DBusMessageIter *it, const char *s)
{ {
@@ -458,7 +490,6 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx)
dbus_uint32_t sym, code, state; dbus_uint32_t sym, code, state;
Keyres res; Keyres res;
char commit[Maxutf], preedit[Maxutf]; char commit[Maxutf], preedit[Maxutf];
u32int ks, mod;
dbus_error_init(&err); dbus_error_init(&err);
if(!dbus_message_get_args(m, &err, if(!dbus_message_get_args(m, &err,
@@ -470,11 +501,8 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx)
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
} }
USED(code); USED(code);
if(state & Relmask || !ctx->focused) if(!processkey(ctx, sym, state, &res))
return handlebool(c, m, 0); return handlebool(c, m, 0);
ks = kget(sym);
mod = mget(state);
sendrequest(ctx, Keypress, ks, mod, &res);
stoutf(&res.commit, commit, sizeof commit); stoutf(&res.commit, commit, sizeof commit);
stoutf(&res.preedit, preedit, sizeof preedit); stoutf(&res.preedit, preedit, sizeof preedit);
if(commit[0] != '\0') if(commit[0] != '\0')
@@ -531,7 +559,6 @@ handlecursor(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{ {
DBusError err; DBusError err;
dbus_int32_t x, y, w, h; dbus_int32_t x, y, w, h;
Keyres res;
dbus_error_init(&err); dbus_error_init(&err);
if(!dbus_message_get_args(m, &err, if(!dbus_message_get_args(m, &err,
@@ -545,12 +572,7 @@ handlecursor(DBusConnection *c, DBusMessage *m, Ictx *ctx)
if(w < 0 || h < 0) if(w < 0 || h < 0)
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS, return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"cursor dimensions must not be negative"); "cursor dimensions must not be negative");
ctx->caret.valid = 1; setcursor(ctx, x, y, h);
ctx->caret.x = x;
ctx->caret.y = y;
ctx->caret.h = h;
if(ctx->focused)
sendrequest(ctx, Keycaret, 0, 0, &res);
return handlenoop(c, m); return handlenoop(c, m);
} }
@@ -671,7 +693,7 @@ newconn(DBusServer *s, DBusConnection *c, void *_)
static void static void
pruneconns(void) pruneconns(void)
{ {
int i, j, k; int i, j;
j = 0; j = 0;
for(i = 0; i < nconns; i++){ for(i = 0; i < nconns; i++){
@@ -679,9 +701,7 @@ pruneconns(void)
conns[j++] = conns[i]; conns[j++] = conns[i];
continue; continue;
} }
for(k = 0; k < nelem(contexts); k++) dropconncontexts(conns[i]);
if(contexts[k].conn == conns[i])
dropcontext(&contexts[k]);
dbus_connection_unref(conns[i]); dbus_connection_unref(conns[i]);
} }
nconns = j; nconns = j;

View File

@@ -34,6 +34,12 @@ testenginepreedit(Str *preedit)
impre(preedit); impre(preedit);
} }
void
testenginecaret(Caret *at)
{
*at = caret;
}
void void
vietnamese_state_lifetime(struct ct *t) vietnamese_state_lifetime(struct ct *t)
{ {

View File

@@ -1,71 +1,354 @@
#include "../ibus.c" #include "../ibus.c"
#include "../cutest/cutest.h" #include "../cutest/cutest.h"
void testengineinit(int);
void testenginehandle(Keyreq*);
void* testengineowner(void);
void testenginepreedit(Str*);
void testenginecaret(Caret*);
enum
{
Testctrlmask = 1<<2,
};
typedef struct Ibusfix Ibusfix;
struct Ibusfix
{
Channel *oldreply;
Channel *trace;
Channel *stop;
Channel *done;
DBusConnection *c1;
DBusConnection *c2;
int pumpactive;
};
static void
enginepump(void *arg)
{
Ibusfix *f;
Keyreq req;
uchar token;
Alt alts[] = {
{keyc, &req, CHANRCV, nil},
{nil, &token, CHANRCV, nil},
{nil, nil, CHANEND, nil},
};
f = arg;
alts[1].c = f->stop;
for(;;)
switch(alt(alts)){
case 0:
chansend(f->trace, &req);
testenginehandle(&req);
break;
case 1:
chansend(f->done, &token);
return;
}
}
static int
ibusbegin(struct ct *t, Ibusfix *f)
{
Drawcmd dc;
memset(f, 0, sizeof *f);
memset(contexts, 0, sizeof contexts);
memset(conns, 0, sizeof conns);
nconns = 0;
while(channbrecv(drawc, &dc) > 0)
;
testengineinit(LangEN);
f->oldreply = replyc;
replyc = chancreate(sizeof(Keyres), 0);
f->trace = chancreate(sizeof(Keyreq), Maxcontexts);
f->stop = chancreate(sizeof(uchar), 0);
f->done = chancreate(sizeof(uchar), 0);
f->pumpactive = threadcreate(enginepump, f, 8192) >= 0;
f->c1 = malloc(1);
f->c2 = malloc(1);
return CT_CHECK(t, f->pumpactive && f->c1 != nil && f->c2 != nil);
}
static void
ibusend(Ibusfix *f)
{
Drawcmd dc;
Keyreq req;
uchar token;
int i;
if(f->pumpactive){
for(i = 0; i < nelem(contexts); i++)
if(contexts[i].conn != nil)
dropcontext(&contexts[i]);
while(channbrecv(f->trace, &req) > 0)
;
token = 0;
chansend(f->stop, &token);
chanrecv(f->done, &token);
}
memset(contexts, 0, sizeof contexts);
memset(conns, 0, sizeof conns);
nconns = 0;
testengineinit(LangEN);
while(channbrecv(drawc, &dc) > 0)
;
free(f->c1);
free(f->c2);
chanfree(replyc);
replyc = f->oldreply;
chanfree(f->trace);
chanfree(f->stop);
chanfree(f->done);
}
static Keyreq
nexttrace(struct ct *t, Ibusfix *f, int op, Ictx *ctx)
{
Keyreq req;
memset(&req, 0, sizeof req);
if(!CT_CHECK(t, channbrecv(f->trace, &req) > 0))
return req;
CT_EQ_INT(t, op, req.op);
CT_EQ_PTR(t, ctx, req.owner);
CT_EQ_PTR(t, replyc, req.reply);
return req;
}
static void
notrace(struct ct *t, Ibusfix *f)
{
Keyreq req;
CT_CHECK(t, channbrecv(f->trace, &req) <= 0);
}
static void
checkpreedit(struct ct *t, char *want, Keyres *res)
{
char got[Maxutf];
stoutf(&res->preedit, got, sizeof got);
CT_EQ_STR(t, want, got);
}
static void
checkenginepreedit(struct ct *t, char *want)
{
char got[Maxutf];
Str preedit;
testenginepreedit(&preedit);
stoutf(&preedit, got, sizeof got);
CT_EQ_STR(t, want, got);
}
static Keyres
contextkey(struct ct *t, Ibusfix *f, Ictx *ctx, u32int sym, u32int state)
{
Keyres res;
memset(&res, 0, sizeof res);
CT_CHECK(t, processkey(ctx, sym, state, &res));
nexttrace(t, f, Keypress, ctx);
return res;
}
void void
ibus_context_lifecycle(struct ct *t) ibus_context_lifecycle(struct ct *t)
{ {
Channel *oldkeyc, *oldreplyc; Ibusfix f;
DBusConnection *c1, *c2; Ictx *a, *b, *reused;
Ictx *a, *b;
Keyreq req; Keyreq req;
Keyres res; Keyres res;
Caret at;
oldkeyc = keyc; if(!ibusbegin(t, &f))
oldreplyc = replyc;
keyc = chancreate(sizeof(Keyreq), 2);
replyc = chancreate(sizeof(Keyres), 2);
memset(contexts, 0, sizeof contexts);
c1 = malloc(1);
c2 = malloc(1);
if(!CT_CHECK(t, c1 != nil && c2 != nil))
goto cleanup; goto cleanup;
a = newcontext(c1, "/context/one"); a = newcontext(f.c1, "/context/one");
b = newcontext(c2, "/context/one"); b = newcontext(f.c2, "/context/one");
if(!CT_CHECK(t, a != nil && b != nil && a != b)) if(!CT_CHECK(t, a != nil && b != nil && a != b))
goto cleanup; goto cleanup;
CT_EQ_PTR(t, a, findcontext(c1, "/context/one")); CT_EQ_PTR(t, a, findcontext(f.c1, "/context/one"));
CT_EQ_PTR(t, b, findcontext(c2, "/context/one")); CT_EQ_PTR(t, b, findcontext(f.c2, "/context/one"));
CT_EQ_PTR(t, nil, findcontext(c1, "/context/missing")); CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/missing"));
a->caret.valid = 1;
releasecontext(a);
CT_CHECK(t, channbrecv(keyc, &req) <= 0);
CT_CHECK(t, !a->caret.valid);
a->focused = 1;
a->caret.valid = 1;
memset(&res, 0, sizeof res); memset(&res, 0, sizeof res);
chansend(replyc, &res); CT_CHECK(t, !processkey(a, 'x', 0, &res));
releasecontext(a); CT_CHECK(t, !res.eaten);
if(CT_CHECK(t, channbrecv(keyc, &req) > 0)){ notrace(t, &f);
CT_EQ_PTR(t, a, req.owner); CT_EQ_PTR(t, nil, testengineowner());
CT_EQ_INT(t, Keyrelease, req.op);
CT_EQ_PTR(t, replyc, req.reply);
}
CT_CHECK(t, !a->focused);
CT_CHECK(t, !a->caret.valid);
releasecontext(a);
CT_CHECK(t, channbrecv(keyc, &req) <= 0);
dropcontext(a); setcursor(a, 10, 20, 14);
CT_CHECK(t, channbrecv(keyc, &req) <= 0); notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(c1, "/context/one")); CT_CHECK(t, a->caret.valid);
CT_EQ_PTR(t, a, newcontext(c1, "/context/reused")); a->focused = 1;
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(a, 'x', Relmask, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
CT_EQ_PTR(t, nil, testengineowner());
memset(&res, 0, sizeof res);
CT_CHECK(t, processkey(a, 'x', 0, &res));
req = nexttrace(t, &f, Keypress, a);
CT_CHECK(t, !res.eaten);
CT_CHECK(t, req.caret.valid);
CT_EQ_INT(t, 10, req.caret.x);
CT_EQ_INT(t, 20, req.caret.y);
CT_EQ_INT(t, 14, req.caret.h);
CT_EQ_PTR(t, a, testengineowner());
testenginecaret(&at);
CT_CHECK(t, at.valid);
CT_EQ_INT(t, 10, at.x);
CT_EQ_INT(t, 20, at.y);
res = contextkey(t, &f, a, 'n', Testctrlmask);
CT_CHECK(t, res.eaten);
contextkey(t, &f, a, 'k', 0);
res = contextkey(t, &f, a, 'a', 0);
checkpreedit(t, "", &res);
b->focused = 1; b->focused = 1;
chansend(replyc, &res); res = contextkey(t, &f, b, 'n', 0);
dropcontext(b); checkpreedit(t, "", &res);
if(CT_CHECK(t, channbrecv(keyc, &req) > 0)){ CT_EQ_PTR(t, b, testengineowner());
CT_EQ_PTR(t, b, req.owner);
CT_EQ_INT(t, Keyrelease, req.op); setcursor(b, 50, 60, 12);
} req = nexttrace(t, &f, Keycaret, b);
CT_EQ_PTR(t, nil, findcontext(c2, "/context/one")); CT_CHECK(t, req.caret.valid);
CT_EQ_INT(t, 50, req.caret.x);
testenginecaret(&at);
CT_CHECK(t, at.valid);
CT_EQ_INT(t, 50, at.x);
memset(&res, 0, sizeof res);
sendrequest(a, Keyreset, 0, 0, &res);
nexttrace(t, &f, Keyreset, a);
checkpreedit(t, "", &res);
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
setcursor(a, 90, 100, 20);
nexttrace(t, &f, Keycaret, a);
testenginecaret(&at);
CT_EQ_INT(t, 50, at.x);
CT_EQ_INT(t, 60, at.y);
releasecontext(a);
nexttrace(t, &f, Keyrelease, a);
CT_CHECK(t, !a->focused);
CT_CHECK(t, !a->caret.valid);
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
releasecontext(a);
notrace(t, &f);
dropcontext(a);
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/one"));
reused = newcontext(f.c1, "/context/stale-destroy");
CT_EQ_PTR(t, a, reused);
reused->focused = 1;
dropcontext(reused);
nexttrace(t, &f, Keyrelease, reused);
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/stale-destroy"));
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
reused = newcontext(f.c1, "/context/reused");
CT_EQ_PTR(t, a, reused);
reused->focused = 1;
dropconncontexts(f.c1);
nexttrace(t, &f, Keyrelease, reused);
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/reused"));
checkenginepreedit(t, "");
CT_EQ_PTR(t, b, testengineowner());
dropconncontexts(f.c1);
notrace(t, &f);
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(b, 'x', Relmask, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
CT_EQ_PTR(t, b, testengineowner());
cleanup: cleanup:
memset(contexts, 0, sizeof contexts); ibusend(&f);
free(c1); }
free(c2);
chanfree(keyc); void
chanfree(replyc); ibus_active_release_lifecycle(struct ct *t)
keyc = oldkeyc; {
replyc = oldreplyc; Ibusfix f;
Ictx *ctx, *reused;
Keyres res;
if(!ibusbegin(t, &f))
goto cleanup;
ctx = newcontext(f.c1, "/context/reset");
if(!CT_CHECK(t, ctx != nil))
goto cleanup;
ctx->focused = 1;
contextkey(t, &f, ctx, 'n', Testctrlmask);
res = contextkey(t, &f, ctx, 'n', 0);
checkpreedit(t, "", &res);
memset(&res, 0, sizeof res);
sendrequest(ctx, Keyreset, 0, 0, &res);
nexttrace(t, &f, Keyreset, ctx);
checkpreedit(t, "", &res);
checkenginepreedit(t, "");
CT_CHECK(t, ctx->focused);
CT_EQ_PTR(t, ctx, testengineowner());
res = contextkey(t, &f, ctx, 'k', 0);
checkpreedit(t, "k", &res);
CT_EQ_PTR(t, ctx, testengineowner());
releasecontext(ctx);
nexttrace(t, &f, Keyrelease, ctx);
CT_CHECK(t, !ctx->focused);
checkenginepreedit(t, "");
CT_EQ_PTR(t, nil, testengineowner());
releasecontext(ctx);
notrace(t, &f);
dropcontext(ctx);
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/reset"));
reused = newcontext(f.c1, "/context/destroy");
CT_EQ_PTR(t, ctx, reused);
reused->focused = 1;
contextkey(t, &f, reused, 'n', 0);
CT_EQ_PTR(t, reused, testengineowner());
dropcontext(reused);
nexttrace(t, &f, Keyrelease, reused);
CT_EQ_PTR(t, nil, testengineowner());
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/destroy"));
reused = newcontext(f.c1, "/context/prune");
CT_EQ_PTR(t, ctx, reused);
CT_EQ_PTR(t, nil, testengineowner());
memset(&res, 0, sizeof res);
CT_CHECK(t, !processkey(reused, 'x', 0, &res));
CT_CHECK(t, !res.eaten);
notrace(t, &f);
reused->focused = 1;
contextkey(t, &f, reused, 'n', 0);
CT_EQ_PTR(t, reused, testengineowner());
dropconncontexts(f.c1);
nexttrace(t, &f, Keyrelease, reused);
CT_EQ_PTR(t, nil, testengineowner());
notrace(t, &f);
CT_EQ_PTR(t, nil, findcontext(f.c1, "/context/prune"));
dropconncontexts(f.c1);
notrace(t, &f);
cleanup:
ibusend(&f);
} }

View File

@@ -72,3 +72,4 @@ void ipc_response_fragmented_and_truncated(struct ct*);
void ipc_broken_peer_send(struct ct*); void ipc_broken_peer_send(struct ct*);
void server_connection_ownership(struct ct*); void server_connection_ownership(struct ct*);
void ibus_context_lifecycle(struct ct*); void ibus_context_lifecycle(struct ct*);
void ibus_active_release_lifecycle(struct ct*);

View File

@@ -126,6 +126,7 @@ static const struct ct_test tests[] = {
{ "ipc/broken-peer-send", ipc_broken_peer_send }, { "ipc/broken-peer-send", ipc_broken_peer_send },
{ "server/connection-ownership", server_connection_ownership }, { "server/connection-ownership", server_connection_ownership },
{ "ibus/context-lifecycle", ibus_context_lifecycle }, { "ibus/context-lifecycle", ibus_context_lifecycle },
{ "ibus/active-release-lifecycle", ibus_active_release_lifecycle },
}; };
void void