Files
strans/tests/ipc_live_test.c
Hojun-Cho 910bf51347 ipc, srv, gtk: the client's text goes over and a take-back comes back
The engine can reach a Hanja reading back into the text the client
already holds, but only if the frontend hands that text over and can
take some of it away again.  GTK 3 has both: retrieve-surrounding
brings the text around the cursor and delete-surrounding removes runes
before it, and a widget that answers neither leaves the text empty, so
nothing is ever reached into or taken from it.

The wire grows a control frame for the text, sent like the caret only
when it changes, and one byte in every response for the runes to take
back.  That byte moves the length fields along, so the version goes to
2: an old daemon and a new module, either way round, fail the handshake
and the module falls through to GtkIMContextSimple rather than misread
a frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:31:31 +09:00

391 lines
8.6 KiB
C

#define _GNU_SOURCE
#include <errno.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include "ipc.h"
#include "live.h"
enum
{
Maxclients = 64,
Cappreedit = 1,
};
typedef struct Response Response;
struct Response
{
int eaten;
int del;
char commit[Ipcfieldmax+1];
char preedit[Ipcfieldmax+1];
};
static size_t
getlen(unsigned char p[Ipclensz])
{
return p[0] | (p[1] << 8);
}
static int
readresponseuntil(int fd, int want, Response *res, int64_t deadline,
int quiet)
{
unsigned char hdr[Ipcresphdrsz], len[Ipclensz];
size_t n;
int rv;
memset(res, 0, sizeof *res);
rv = readuntil(fd, hdr, sizeof hdr, deadline);
if(rv <= 0){
if(!quiet)
fail("read response header: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
res->eaten = hdr[0] != 0;
res->del = hdr[1];
n = getlen(hdr + 2);
if(n > Ipcfieldmax){
fail("invalid commit length %zu", n);
return -1;
}
if(n != 0){
rv = readuntil(fd, res->commit, n, deadline);
if(rv <= 0){
if(!quiet)
fail("read commit: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
}
res->commit[n] = '\0';
if(!want)
return 1;
rv = readuntil(fd, len, sizeof len, deadline);
if(rv <= 0){
if(!quiet)
fail("read preedit length: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
n = getlen(len);
if(n > Ipcfieldmax){
fail("invalid preedit length %zu", n);
return -1;
}
if(n != 0){
rv = readuntil(fd, res->preedit, n, deadline);
if(rv <= 0){
if(!quiet)
fail("read preedit: %s",
rv == 0 ? "peer closed" : strerror(errno));
return rv;
}
}
res->preedit[n] = '\0';
return 1;
}
static int
sendkey(int fd, int want, uint32_t mod, uint32_t key)
{
unsigned char req[Ipcreqsz];
ipcpackreq(req, want, mod, key);
return ipcsend(fd, req, sizeof req) == 0;
}
static int
sendreset(int fd, int want)
{
unsigned char req[Ipcreqsz];
ipcpackreset(req, want);
return ipcsend(fd, req, sizeof req) == 0;
}
static int
sendcap(int fd, int cap)
{
unsigned char req[Ipcreqsz];
ipcpackcap(req, cap);
return ipcsend(fd, req, sizeof req) == 0;
}
static int
sendsurround(int fd, char *text)
{
unsigned char req[Ipcreqsz];
size_t n;
n = strlen(text);
ipcpacksurround(req, n);
return ipcsend(fd, req, sizeof req) == 0 &&
(n == 0 || ipcsend(fd, text, n) == 0);
}
static int
expectresponse(int fd, int want, int eaten, char *commit, char *preedit,
char *where)
{
Response res;
if(readresponseuntil(fd, want, &res, nowms() + Calltimeout, 0) != 1)
return 0;
if(res.eaten != eaten || strcmp(res.commit, commit) != 0 ||
(want && strcmp(res.preedit, preedit) != 0))
return fail("%s: eaten=%d commit=%s preedit=%s",
where, res.eaten, res.commit, res.preedit);
return 1;
}
static int
requestreset(int fd, int want, int eaten, char *commit, char *preedit,
char *where)
{
if(!sendreset(fd, want))
return fail("send %s: %s", where, strerror(errno));
return expectresponse(fd, want, eaten, commit, preedit, where);
}
static int
requestkey(int fd, int want, uint32_t mod, uint32_t key, int eaten,
char *commit, char *preedit, char *where)
{
if(!sendkey(fd, want, mod, key))
return fail("send %s: %s", where, strerror(errno));
return expectresponse(fd, want, eaten, commit, preedit, where);
}
/*
* The Korean habit: 한자 typed, then the Hanja key. The 한 is the
* client's by then, so the daemon must ask for it back and answer with
* the whole word.
*/
static int
requesthanja(int fd)
{
Response res;
if(!sendsurround(fd, "저는 한"))
return fail("send surrounding text: %s", strerror(errno));
if(!requestkey(fd, 1, Mctrl, 's', 1, "", "", "select Korean") ||
!requestkey(fd, 1, 0, 'w', 1, "", "", "jamo") ||
!requestkey(fd, 1, 0, 'k', 1, "", "", "syllable") ||
!requestkey(fd, 1, Mctrl, 'h', 1, "", "", "Hanja search"))
return 0;
if(!sendkey(fd, 1, 0, Kret))
return fail("send Hanja pick: %s", strerror(errno));
if(readresponseuntil(fd, 1, &res, nowms() + Calltimeout, 0) != 1)
return 0;
if(strcmp(res.commit, "漢字") != 0 || res.del != 1)
return fail("Hanja pick committed %s and took back %d",
res.commit, res.del);
return 1;
}
static int
tryclient(char *path, int64_t deadline, int *client)
{
Response res;
int fd, rv;
fd = connectsocket(path, deadline);
if(fd < 0)
return errno == ETIMEDOUT ? -1 : 0;
if(!sendkey(fd, 1, 0, 0)){
close(fd);
return 0;
}
rv = readresponseuntil(fd, 1, &res, deadline, 1);
if(rv != 1){
close(fd);
return rv < 0 && errno == ETIMEDOUT ? -1 : 0;
}
if(res.eaten || res.commit[0] != '\0' || res.preedit[0] != '\0'){
close(fd);
fail("replacement client returned invalid modifier response");
return -1;
}
*client = fd;
return 1;
}
static int
waitslot(char *path, int *client, char *where)
{
int rv;
int64_t deadline;
deadline = nowms() + Calltimeout;
for(;;){
rv = tryclient(path, deadline, client);
if(rv > 0)
return 1;
if(rv < 0 || leftms(deadline) == 0)
return fail("%s: timed out waiting for a worker slot", where);
}
}
static int
overflowrejected(char *path)
{
struct pollfd pfd;
unsigned char byte;
ssize_t n;
int fd, timeout;
int64_t deadline;
deadline = nowms() + Calltimeout;
fd = connectsocket(path, deadline);
if(fd < 0)
return fail("overflow connect: %s", strerror(errno));
if(!sendkey(fd, 1, 0, 0)){
close(fd);
return 1;
}
for(;;){
timeout = leftms(deadline);
if(timeout == 0){
close(fd);
return fail("overflow client was not rejected");
}
pfd.fd = fd;
pfd.events = POLLIN;
pfd.revents = 0;
n = poll(&pfd, 1, timeout);
if(n < 0 && errno == EINTR)
continue;
if(n <= 0){
close(fd);
return fail("poll overflow client: %s",
n == 0 ? "timed out" : strerror(errno));
}
n = recv(fd, &byte, 1, 0);
if(n < 0 && errno == EINTR)
continue;
if(n == 0 || (n < 0 && (errno == ECONNRESET || errno == EPIPE))){
close(fd);
return 1;
}
if(n < 0){
close(fd);
return fail("read overflow rejection: %s", strerror(errno));
}
close(fd);
return fail("overflow client received a response");
}
}
static int
runsmoke(char *path)
{
int client, ok;
client = connectsocket(path, nowms() + Calltimeout);
if(client < 0)
return fail("connect client: %s", strerror(errno));
ok = sendcap(client, Cappreedit) &&
expectresponse(client, 1, 1, "", "", "negotiate preedit") &&
requestkey(client, 1, Mctrl, 'n', 1, "", "",
"select Japanese") &&
requestkey(client, 1, 0, 'k', 1, "", "k", "preedit") &&
requestreset(client, 1, 1, "k", "", "reset") &&
requesthanja(client);
close(client);
return ok;
}
static int
runcapacity(char *path)
{
int client[Maxclients];
int i, ok;
for(i = 0; i < Maxclients; i++)
client[i] = -1;
ok = 0;
client[0] = connectsocket(path, nowms() + Calltimeout);
if(client[0] < 0){
fail("connect first client: %s", strerror(errno));
goto out;
}
if(!requestkey(client[0], 1, Mctrl, 'n', 1, "", "",
"select Japanese"))
goto out;
for(i = 1; i < Maxclients; i++){
client[i] = connectsocket(path, nowms() + Calltimeout);
if(client[i] < 0){
fail("connect capacity client %d: %s", i, strerror(errno));
goto out;
}
if(!requestkey(client[i], 1, 0, 0, 0, "", "",
"capacity modifier"))
goto out;
}
if(!overflowrejected(path))
goto out;
close(client[Maxclients-1]);
client[Maxclients-1] = -1;
if(!waitslot(path, &client[Maxclients-1], "inactive slot recovery"))
goto out;
close(client[0]);
client[0] = -1;
if(!waitslot(path, &client[0], "active slot recovery") ||
!requestkey(client[0], 1, 0, 'a', 1, "", "",
"post-disconnect composition"))
goto out;
ok = 1;
out:
for(i = 0; i < Maxclients; i++)
if(client[i] >= 0)
close(client[i]);
return ok;
}
int
main(int argc, char **argv)
{
Daemon daemon;
Live live;
int capacity, ok;
testname = "ipc_live_test";
capacity = argc == 4 && strcmp(argv[1], "--capacity") == 0;
if((!capacity && argc != 3) || (capacity && argc != 4)){
fprintf(stderr,
"usage: ipc_live_test [--capacity] strans mapdir\n");
return 2;
}
daemoninit(&daemon, "daemon");
ok = livesetup(&live, "ipc") &&
startdaemon(&live, &daemon, argv[1+capacity], argv[2+capacity]) &&
waitready(&live, &daemon, NULL, 0);
if(ok)
ok = capacity ? runcapacity(live.socket) : runsmoke(live.socket);
if(!ok)
showerrors(&daemon);
if(!stopdaemon(&daemon))
ok = 0;
/* SIGTERM must leave no socket behind. */
if(access(live.socket, F_OK) == 0)
ok = fail("socket %s survived the daemon", live.socket);
if(!closeerrors(&daemon))
ok = 0;
if(!liveclean(&live))
ok = 0;
if(!ok)
return 1;
printf("ipc %s: ok\n", capacity ? "connection capacity" : "live smoke");
return 0;
}