Skip to content

플러그인 작성하기 ​

이 가이드를 마치면 C 라이브러리로 작은 플러그인을 만들어 zeropty 서버에 붙이고, 플러그인이 더한 명령을 CLI에서 부르고, 서버를 띄울 때마다 같이 실행되게 할 수 있어요. 플러그인은 tally예요. 만들어진 세션과 끝난 세션을 세고, 지금 살아 있는 세션이 몇 개인지 알려 줘요.

시작하기 전에 ​

  • PATH에 zeropty가 있는 Mac이 필요해요.
  • bindings/c/include/zeropty.h와 examples/plugins/tally.c가 있는 zeropty 저장소가 필요해요. 명령은 그 폴더에서 실행하세요. 플러그인 라이브러리에는 헤더만 있으면 돼요. libzeropty_c에 링크하지 않아요.
  • Xcode Command Line Tools의 cc가 필요해요.

1. 붙이는 방법 고르기 ​

플러그인은 세션 이벤트를 듣고 명령을 더해요. 더한 명령은 내장 명령처럼 CLI, 소켓, 임베디드 호출에서 불러요. 붙이는 방법은 네 가지이고, 모두 같은 등록부, 이벤트, 권한 수준, 격리 규칙을 써요.

방법언어어디서 돌아요예제
소켓 (프로세스 밖)아무 언어 (소켓과 JSON)별도 프로세스examples/plugins/hello.py, examples/plugins/notify.mjs
플러그인 라이브러리 (.dylib)C, 또는 C 함수를 내보낼 수 있는 언어zeropty plugin-run으로 별도 프로세스, 또는 호스트 안examples/plugins/tally.c
C ABI(application binary interface) 콜백C, 또는 C 함수를 넘길 수 있는 언어호스트 프로세스, 플러그인 스레드examples/plugins/count.c
Rust 트레이트Rust호스트 프로세스, 플러그인 스레드examples/plugin_activity.rs

이 가이드는 라이브러리를 만들어요. 같은 .dylib이 서버 옆에서도, 임베딩 호스트 안에서도 실행되기 때문이에요. 플러그인 코드는 엔진 스레드에서 돌지 않으므로, 느리거나 크래시하는 플러그인이 내 세션을 멈추지 않아요.

2. 플러그인 쓰기 ​

라이브러리의 진입점은 zeropty_plugin_init 하나예요. 매니페스트(이름, 이벤트, 명령)와 콜백을 채워요. 콜백은 JSON 메시지를 한 번에 하나씩, 플러그인 자신의 스레드에서 받아요.

c
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "zeropty.h"

static const zeropty_plugin_api *zp; /* from the host: host_command and free */
static atomic_int created, ended;

/* One JSON message at a time, on the plugin's own thread (a real plugin would use a JSON parser). */
static char *on_message(void *user, const char *msg, zeropty_host *host) {
    (void)user;
    if (strstr(msg, "{\"event\":\"session-created\"") == msg) created++;
    if (strstr(msg, "{\"event\":\"session-exited\"") == msg) ended++;
    if (strncmp(msg, "{\"call\"", 7) != 0) return NULL; /* start, events, stop: nothing to answer */
    /* Asks zeropty, from inside the plugin, for the sessions running now: one "x" per session. */
    char *r = zp->host_command(host, "{\"argv\":[\"list-sessions\",\"-F\",\"x\"],\"cwd\":\"/\"}");
    int live = 0;
    const char *out = r ? strstr(r, "\"out\":\"") : NULL;
    for (const char *p = out ? out + 7 : ""; *p && *p != '"'; p++) live += *p == 'x';
    zp->free(r);
    char *reply = malloc(128); /* zeropty frees it */
    snprintf(reply, 128, "{\"code\":0,\"out\":\"created %d, ended %d, live %d\\n\",\"err\":\"\"}", created, ended, live);
    return reply;
}

int zeropty_plugin_init(const zeropty_plugin_api *api, zeropty_plugin_def *def) {
    if (api->api < 1) return 1;
    zp = api;
    def->manifest_json = "{\"name\":\"tally\",\"events\":[\"session-created\",\"session-exited\"],\"commands\":[\"tally\"]}";
    def->callback = on_message;
    def->user = NULL;
    return 0;
}

콜백은 {"start":{"api":1}}을 가장 먼저 받고, 그다음 이벤트와 호출을 받고, {"stop":true}를 마지막으로 받아요. call에는 code, out, err가 든 답을 malloc해서 돌려주세요. zeropty가 해제해요. 그 밖의 메시지에는 NULL을 돌려주세요.

host_command는 플러그인 안에서 zeropty 명령을 플러그인의 권한(access) 범위에서 실행해요. 위 매니페스트는 권한을 밝히지 않았으므로 기본값인 read를 받아요. list-sessions와 capture-pane은 되지만 send-keys는 안 돼요. 키를 보내려면 매니페스트에 "access":["input"]을 더하세요.

3. 빌드하기 ​

헤더만으로 라이브러리를 빌드해요.

sh
cc -dynamiclib -O2 -Ibindings/c/include -o /tmp/tally.dylib examples/plugins/tally.c

4. 서버에 붙이기 ​

서버가 없으면 하나 띄우고, plugin-run으로 라이브러리를 실행한 다음 그 명령을 불러요.

sh
zeropty new-session -d -s a sh            # starts the server if none is running
zeropty plugin-run /tmp/tally.dylib &
zeropty tally

plugin-run은 라이브러리를 자기 프로세스에 불러와서 소켓으로 서버에 붙여요. 라이브러리가 크래시하면 그 프로세스만 죽어요. 서버가 끝나면 plugin-run도 끝나요.

5. 서버를 띄울 때마다 같이 시작하기 ​

라이브러리를 플러그인 디렉터리에 넣고 ZEROPTY_PLUGIN_DIR로 그 디렉터리를 가리키세요. 서버는 그 안의 플러그인을 시작하고, 끝나면 다시 시작해요.

sh
export ZEROPTY_PLUGIN_DIR=~/.zeropty/plugins               # put this in ~/.zshrc to keep it always on
mkdir -p ~/.zeropty/plugins
cp /tmp/tally.dylib ~/.zeropty/plugins/                  # a .dylib is started with zeropty plugin-run
zeropty restart-server                                     # takes effect from the next server

기본은 꺼져 있어요

ZEROPTY_PLUGIN_DIR가 없으면 ~/.zeropty/plugins가 있어도 서버는 아무것도 실행하지 않아요. 이 값을 지정하는 건 그 디렉터리의 코드를 내 사용자 권한으로 실행해도 된다는 동의예요. 다른 사용자가 쓸 수 있는 디렉터리와 파일은 건너뛰어요.

  • 서버는 자기를 띄운 셸에서 ZEROPTY_PLUGIN_DIR를 가져가요. 서버를 띄우는 new-session이나 restart-server를 실행한 셸이에요.
  • 실행 파일과 .dylib 파일은 이름순으로 시작해요. 각 플러그인의 출력은 디렉터리의 NAME.log로 가요.
  • 금방 끝나는 플러그인은 0.5초 뒤에 다시 시작하고, 이후 두 배씩 늘려 최대 30초까지 기다려요.
  • 플러그인은 restart-server를 지나도 남아요. 옛 서버의 plugin-run은 옛 서버와 함께 끝나고, 새 서버가 소켓을 넘겨받은 뒤 자기 플러그인을 시작해요.

6. 프로그램에서 부르기 ​

플러그인 명령은 다른 명령과 똑같이 불러요. 엔진을 내장한 호스트에서는 부른 스레드가 기다릴지 말지 고를 수 있어요.

호스트답을 기다려요바로 돌아와요
Czeropty_commandzeropty_command_start → zeropty_call_poll → zeropty_call_free
Rust Embedded, Polledcommand(argv, cwd)command_start(argv, cwd) -> Call
Node (client/embedded.mjs)zeroptySync(argv)await zeropty(argv)

엔진을 내장한 호스트는 zeropty_load_plugin(a, "/tmp/tally.dylib")로 같은 라이브러리를 자기 프로세스에 불러올 수도 있어요. 그러면 라이브러리의 크래시는 그 프로세스의 크래시가 돼요.

확인하기 ​

세션 두 개를 시작하고 하나를 끝낸 다음, 명령을 부르고 플러그인 목록을 보세요.

sh
zeropty new-session -d -s b sh
zeropty new-session -d -s c sh
zeropty kill-session -t '=c:'
zeropty tally
zeropty list-plugins

플러그인이 시작된 뒤의 개수가 보여야 해요. live는 실행 중인 모든 세션을 세므로, a와 b만 남았을 때는 이렇게 나와요.

text
created 2, ended 1, live 2

list-plugins는 플러그인마다 name: state (api N) events=... commands=... access=... dropped=N 형태로 한 줄씩 출력해요. tally: running을 찾아보세요.

문제가 생기면 ​

증상해결
tally가 unknown command예요플러그인이 붙어 있지 않아요. zeropty list-plugins를 확인하세요. restart-server 직후에는 수십 ms 안에 플러그인이 다시 붙어요.
failed (stuck in a hook ...)콜백이 마감 시간(10초, ZEROPTY_PLUGIN_TIMEOUT_MS)보다 오래 실행됐어요. 먼저 "started"로 답하고 결과는 다른 명령으로 가져가게 하세요.
plugin tally lacks access input매니페스트에 권한을 더하세요. 예: "access":["input"]
bad manifest: ...JSON 매니페스트에는 name, events, commands, access, api만 쓸 수 있어요. "event" 같은 오타가 없는지 확인하세요.
dropped=가 계속 늘어요플러그인이 이벤트를 너무 느리게 읽어요. 큐에는 이벤트 1024개나 출력 8MB까지 들어가요. output보다 activity를 쓰세요.
플러그인 디렉터리가 아무 일도 안 해요서버를 띄운 셸에 ZEROPTY_PLUGIN_DIR가 없었어요. 지정한 뒤 zeropty restart-server를 실행하세요.

다음 단계 ​

MIT 또는 Apache-2.0 라이선스로 배포해요.