Notification support

This commit is contained in:
Aaron Cook 2026-05-24 21:17:10 -05:00
commit 297c8f800e
7 changed files with 286 additions and 102 deletions

View file

@ -17,7 +17,8 @@ INCLUDES= \
SRCS= \ SRCS= \
libraries/cJSON/cJSON.c \ libraries/cJSON/cJSON.c \
src/json_rpc.c \ src/json_rpc.c \
src/main.c src/main.c \
src/pawn_lsp.c
OBJS=$(addprefix $(BUILD_DIR)/,$(addsuffix .o,$(basename $(SRCS)))) OBJS=$(addprefix $(BUILD_DIR)/,$(addsuffix .o,$(basename $(SRCS))))
DEPS=$(addprefix $(BUILD_DIR)/,$(addsuffix .d,$(basename $(SRCS)))) DEPS=$(addprefix $(BUILD_DIR)/,$(addsuffix .d,$(basename $(SRCS))))

View file

@ -9,6 +9,7 @@
#include "cJSON/cJSON.h" #include "cJSON/cJSON.h"
#include "log.h" #include "log.h"
#include "lsp.h"
#include "json_rpc.h" #include "json_rpc.h"
static void _service_request(json_rpc_t *jrpc); static void _service_request(json_rpc_t *jrpc);
@ -39,12 +40,22 @@ json_rpc_init(json_rpc_t *jrpc, int infd, int outfd)
return (0); return (0);
} }
int void
json_rpc_add_method(json_rpc_t *jrpc, const char *method, json_rpc_set_request_handler(json_rpc_t *jrpc,
int (*callback)(json_rpc_request_t *req, cJSON **response, void *arg), int (*callback)(json_rpc_request_t *req, cJSON **resp, void *arg),
void *arg) void *arg)
{ {
return (-1); jrpc->req_handler.callback = callback;
jrpc->req_handler.arg = arg;
}
void
json_rpc_set_notify_handler(json_rpc_t *jrpc,
void (*callback)(json_rpc_request_t *req, void *arg),
void *arg)
{
jrpc->notf_handler.callback = callback;
jrpc->notf_handler.arg = arg;
} }
void void
@ -73,12 +84,6 @@ json_rpc_run(json_rpc_t *jrpc)
sched_yield(); sched_yield();
} }
free(jrpc->buf);
for (size_t i = 0; i < jrpc->method_cnt; i++) {
free(jrpc->methods[i].method);
}
free(jrpc->methods);
} }
void void
@ -89,68 +94,88 @@ json_rpc_cancel(json_rpc_t *jrpc)
static int
_read_headers(int fd, char *buf, size_t len, size_t *content_len, char **content_type)
{
size_t p = 0;
(*content_len) = 0;
(*content_type) = NULL;
while (1) {
size_t rlen = _read_until(fd, &buf[p], len - p, '\n', 1000);
if ((rlen < 2) || (buf[p + rlen - 1] != '\n')) {
return (-1);
}
/* "Content-Length: <val>\r\n" */
if ((rlen > 18) && (!memcmp(&buf[p], "Content-Length: ", 16))) {
(*content_len) = strtoul(&buf[p + 16], NULL, 10);
}
/* "Content-Type: <val>\r\n" */
else if ((rlen > 16) && (!memcmp(&buf[p], "Content-Type: ", 14))) {
(*content_type) = &buf[p + 14];
buf[p + rlen - 2] = '\0';
}
else if (rlen == 2) {
return (0);
}
p += rlen;
if (p >= len) {
return (-1);
}
}
}
static void static void
_service_request(json_rpc_t *jrpc) _service_request(json_rpc_t *jrpc)
{ {
char *buf = jrpc->buf; char headers[1024] = { 0 };
char *len_str; size_t content_length = 0;
char *content; char *content_type = NULL;
size_t p = 0; size_t rlen;
size_t ret;
long content_len;
json_rpc_request_t request; json_rpc_request_t request;
/* read Content-Length: ...\r\n */ if (_read_headers(jrpc->infd, headers, sizeof(headers), &content_length, &content_type)) {
ret = _read_bytes(jrpc->infd, &buf[p], 16, 1000); log_error("Failed to read/parse headers\r\n");
if ((ret != 16) || (memcmp(&buf[p], "Content-Length: ", 16))) {
log_error("Failed to read request header\r\n");
return; return;
} }
p += ret; log_info("Request { length: %ld, type: %s }\r\n", content_length, content_type);
len_str = &buf[p]; if (content_length == 0) {
log_error("Content-Length of 0\r\n");
ret = _read_until(jrpc->infd, &buf[p], jrpc->bufsz - p, '\n', 1000); return;
p += ret; }
if ((ret == 0) || (buf[p-1] != '\n')) { else if (content_length > jrpc->bufsz) {
log_error("Failed to read content length value\r\n"); log_error("Content length too long!\r\n");
while (content_length > 0) {
size_t rlen = content_length > jrpc->bufsz ? jrpc->bufsz : content_length;
rlen = _read_bytes(jrpc->infd, jrpc->buf, rlen, 1000);
content_length -= rlen;
if (rlen == 0) {
break;
}
}
/* TODO error response */
return; return;
} }
content_len = strtol(len_str, NULL, 10); memset(jrpc->buf, 0, jrpc->bufsz);
if (content_len < 0) { rlen = _read_bytes(jrpc->infd, jrpc->buf, content_length, 1000);
log_warn("Invalid content length %ld\r\n", content_len); if (rlen != content_length) {
return;
}
if (content_len > (jrpc->bufsz - p - 3)) {
log_warn("Content too long (%ld)\r\n", content_len);
return;
}
/* Read empty CRLF */
ret = _read_bytes(jrpc->infd, &buf[p], 2, 1000);
if (ret != 2) {
log_error("Failed to read blank line\r\n");
return;
}
p += ret;
content = &buf[p];
ret = _read_bytes(jrpc->infd, &buf[p], content_len, 1000);
if (ret != content_len) {
log_error("Failed to read all of content, exp %ld, got %ld\r\n", log_error("Failed to read all of content, exp %ld, got %ld\r\n",
content_len, ret); content_length, rlen);
return; return;
} }
p += ret;
buf[p] = '\0';
/* JSON parsing */ /* JSON parsing */
request.request = cJSON_Parse(content); request.request = cJSON_Parse(jrpc->buf);
if ((!request.request) || (!cJSON_IsObject(request.request))) { if ((!request.request) || (!cJSON_IsObject(request.request))) {
log_error("Failed to parse request %s\r\n", buf); log_error("Failed to parse request %s\r\n", jrpc->buf);
return; return;
} }
@ -164,8 +189,9 @@ _service_request(json_rpc_t *jrpc)
} }
request.id = cJSON_GetObjectItem(request.request, "id"); request.id = cJSON_GetObjectItem(request.request, "id");
if ((!request.id) || if ((request.id != NULL) &&
((!cJSON_IsNumber(request.id)) && (!cJSON_IsString(request.id)))) { (!cJSON_IsNumber(request.id)) &&
(!cJSON_IsString(request.id))) {
log_error("Invalid ID format\r\n"); log_error("Invalid ID format\r\n");
return; return;
} }
@ -177,19 +203,19 @@ _service_request(json_rpc_t *jrpc)
} }
request.params = cJSON_GetObjectItem(request.request, "params"); request.params = cJSON_GetObjectItem(request.request, "params");
if (request.params) { if ((request.params != NULL) &&
if ((!cJSON_IsArray(request.params)) && (!cJSON_IsObject(request.params))) { (!cJSON_IsArray(request.params)) &&
log_error("Invalid params format\r\n"); (!cJSON_IsObject(request.params))) {
return; log_error("Invalid params format\r\n");
} return;
} }
{ if (request.id) {
/* Request */
cJSON *response = cJSON_CreateObject(); cJSON *response = cJSON_CreateObject();
cJSON *val = NULL; cJSON *val = NULL;
cJSON *body = NULL; cJSON *body = NULL;
const char *resp_token = "error"; const char *resp_token = "error";
json_rpc_method_t *method = NULL;
if (response == NULL) { if (response == NULL) {
cJSON_Delete(request.request); cJSON_Delete(request.request);
@ -207,24 +233,14 @@ _service_request(json_rpc_t *jrpc)
return; return;
} }
for (size_t i = 0; i < jrpc->method_cnt; i++) { if (cJSON_AddStringToObject(response, "jsonrpc", "2.0") == NULL) {
if (!strcmp(jrpc->methods[i].method, request.method->valuestring)) { cJSON_Delete(response);
method = &jrpc->methods[i]; cJSON_Delete(request.request);
break; return;
}
} }
if (!method) { if (jrpc->req_handler.callback(&request, &body, jrpc->req_handler.arg) == 0) {
log_warn("No handler for method \"%s\"\r\n", request.method->valuestring); resp_token = "result";
} else if (method->callback(&request, &body, method->arg) == 0) {
resp_token = "response";
if (body == NULL) {
log_error("No response body given for successful callback\r\n");
cJSON_Delete(response);
cJSON_Delete(request.request);
return;
}
} }
if (body) { if (body) {
@ -238,7 +254,7 @@ _service_request(json_rpc_t *jrpc)
return; return;
} }
if ((cJSON_AddNumberToObject(body, "code", -32601) == NULL) || if ((cJSON_AddNumberToObject(body, "code", LSP_ERR_MethodNotFound) == NULL) ||
(cJSON_AddStringToObject(body, "message", "Not implemented.") == NULL)) { (cJSON_AddStringToObject(body, "message", "Not implemented.") == NULL)) {
cJSON_Delete(body); cJSON_Delete(body);
cJSON_Delete(response); cJSON_Delete(response);
@ -269,8 +285,10 @@ _service_request(json_rpc_t *jrpc)
free(resp_str); free(resp_str);
} }
cJSON_Delete(body);
cJSON_Delete(response); cJSON_Delete(response);
} else {
/* Notify */
jrpc->notf_handler.callback(&request, jrpc->notf_handler.arg);
} }
cJSON_Delete(request.request); cJSON_Delete(request.request);

View file

@ -9,7 +9,8 @@
typedef struct json_rpc_request_struct { typedef struct json_rpc_request_struct {
/** Base request structure, guaranteed to be an object */ /** Base request structure, guaranteed to be an object */
cJSON *request; cJSON *request;
/** Reference to the value of the ID field, guaranteed to be a string or number */ /** Reference to the value of the ID field, guaranteed to be a string, or
* number for requests, and NULL for notifications. */
cJSON *id; cJSON *id;
/** Reference to the value of the method field, guaranteed to be a string */ /** Reference to the value of the method field, guaranteed to be a string */
cJSON *method; cJSON *method;
@ -18,26 +19,21 @@ typedef struct json_rpc_request_struct {
cJSON *params; cJSON *params;
} json_rpc_request_t; } json_rpc_request_t;
typedef struct json_rpc_method_struct {
/** "method" field to match on */
char *method;
/** User arg to pass into callback */
void *arg;
/** Callback to handle the method - 0 return indicates to send back a
* successful result, non-zero will treat the response object as a response
* error object */
int (*callback)(json_rpc_request_t *req, cJSON **response, void *arg);
} json_rpc_method_t;
typedef struct json_rpc_struct { typedef struct json_rpc_struct {
/** Socket to read requests from */ /** Socket to read requests from */
int infd; int infd;
/** Socket to write responses to */ /** Socket to write responses to */
int outfd; int outfd;
/** List of methods added */ /** Registered request handler info */
json_rpc_method_t *methods; struct {
/** Number of methods added */ int (*callback)(json_rpc_request_t *, cJSON **, void *);
size_t method_cnt; void *arg;
} req_handler;
/** Registered notification handler info */
struct {
void (*callback)(json_rpc_request_t *, void *);
void *arg;
} notf_handler;
/** Main server thread */ /** Main server thread */
pthread_t thread; pthread_t thread;
/** Allocated buffer for reading requests */ /** Allocated buffer for reading requests */
@ -53,9 +49,13 @@ typedef struct json_rpc_struct {
int json_rpc_init(json_rpc_t *jrpc, int infd, int outfd); int json_rpc_init(json_rpc_t *jrpc, int infd, int outfd);
int json_rpc_add_method(json_rpc_t *jrpc, const char *method, void json_rpc_set_request_handler(json_rpc_t *jrpc,
int (*callback)(json_rpc_request_t *req, cJSON **response, void *arg), int (*callback)(json_rpc_request_t *req, cJSON **resp, void *arg),
void *arg); void *arg);
void json_rpc_set_notify_handler(json_rpc_t *jrpc,
void (*callback)(json_rpc_request_t *req, void *arg),
void *arg);
void json_rpc_run(json_rpc_t *jrpc); void json_rpc_run(json_rpc_t *jrpc);

View file

@ -1,10 +1,6 @@
#ifndef _LSP_H_ #ifndef _LSP_H_
#define _LSP_H_ #define _LSP_H_
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
/* Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ */ /* Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ */
typedef enum lsp_error_enum { typedef enum lsp_error_enum {

View file

@ -1,14 +1,17 @@
#include <signal.h> #include <signal.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <termios.h> #include <termios.h>
#include <unistd.h> #include <unistd.h>
#include "log.h" #include "log.h"
#include "json_rpc.h" #include "json_rpc.h"
#include "pawn_lsp.h"
void sighandler(int signal); void sighandler(int signal);
static json_rpc_t jrpc; static json_rpc_t jrpc;
static pawn_lsp_t pawn;
FILE *logger = NULL; FILE *logger = NULL;
@ -42,11 +45,18 @@ main(int argc, char **argv)
return (-1); return (-1);
} }
if (pawn_lsp_init(&pawn, &jrpc, argv[1], &argv[2], argc - 2)) {
log_error("Failed to register Pawn language server\r\n");
return (-1);
}
json_rpc_run(&jrpc); json_rpc_run(&jrpc);
pawn_lsp_deinit(&pawn);
if (logger) { if (logger) {
fclose(logger); fclose(logger);
} }
tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_orig); tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_orig);
return (0); return (0);
} }

132
src/pawn_lsp.c Normal file
View file

@ -0,0 +1,132 @@
#include <stdlib.h>
#include <string.h>
#include "cJSON/cJSON.h"
#include "json_rpc.h"
#include "log.h"
#include "pawn_lsp.h"
#define PAWN_LSP_VESRION "v0.0.1"
#define PAWN_LSP_NAME "pawn_lsp"
static int _request_handler(json_rpc_request_t *req, cJSON **resp, void *arg);
static void _notify_handler(json_rpc_request_t *req, void *arg);
static int _initialize(json_rpc_request_t *req, cJSON **resp, void *arg);
static int _shutdown(json_rpc_request_t *req, cJSON **resp, void *arg);
static void _exit_notf(json_rpc_request_t *req, void *arg);
int
pawn_lsp_init(pawn_lsp_t *pawn,
json_rpc_t *jrpc,
const char *pawncc,
char **pawn_args,
int pawn_argc)
{
pawn->jrpc = jrpc;
pawn->pawncc = strdup(pawncc);
/* execv(pawncc <filename> -d0 -v=2 -o/tmp/... [...args] <NULL>)*/
pawn->pawn_args = calloc(pawn_argc + 5, sizeof(char *));
/* pawn->pawn_args[0] = <filename>, set when compiling */
pawn->pawn_args[1] = strdup("-d0");
pawn->pawn_args[2] = strdup("-v=2");
/* pawn->pawn_args[3] = -o/tmp/..., set when compiling */
for (int i = 0; i < pawn_argc; i++) {
pawn->pawn_args[i + 4] = strdup(pawn_args[i]);
}
json_rpc_set_request_handler(jrpc, _request_handler, &pawn);
json_rpc_set_notify_handler(jrpc, _notify_handler, &pawn);
return (0);
}
void
pawn_lsp_deinit(pawn_lsp_t *pawn)
{
free(pawn->pawncc);
for (int i = 0; pawn->pawn_args[i] != NULL; i++) {
free(pawn->pawn_args[i]);
}
free(pawn->pawn_args);
}
static int
_request_handler(json_rpc_request_t *req, cJSON **resp, void *arg)
{
char *method = req->method->valuestring;
log_info("Handle request \"%s\"\r\n", method);
if (!strcmp(method, "initialize")) {
return _initialize(req, resp, arg);
} else if (!strcmp(method, "shutdown")) {
return _shutdown(req, resp, arg);
} else {
log_warn("Unrecognized request method\r\n");
(*resp) = cJSON_CreateNull();
return (-1);
}
}
static void
_notify_handler(json_rpc_request_t *req, void *arg)
{
char *method = req->method->valuestring;
log_info("Handle notification \"%s\"\r\n", method);
if (!strcmp(method, "exit")) {
_exit_notf(req, arg);
} else {
log_warn("Unrecognized notification method\r\n");
}
}
int
_initialize(json_rpc_request_t *req, cJSON **resp, void *arg)
{
cJSON *val;
(*resp) = cJSON_CreateObject();
val = cJSON_AddObjectToObject((*resp), "capabilities");
if (val == NULL) {
return (-1);
}
if (cJSON_AddStringToObject(val, "positionEncoding", "utf-8") == NULL) {
return (-1);
}
val = cJSON_AddObjectToObject((*resp), "serverInfo");
if (val == NULL) {
return (-1);
}
if ((cJSON_AddStringToObject(val, "name", PAWN_LSP_NAME) == NULL) ||
(cJSON_AddStringToObject(val, "version", PAWN_LSP_VESRION) == NULL)) {
return (-1);
}
return (0);
}
static int
_shutdown(json_rpc_request_t *req, cJSON **resp, void *arg)
{
(*resp) = cJSON_CreateNull();
return (0);
}
static void
_exit_notf(json_rpc_request_t *req, void *arg)
{
pawn_lsp_t *pawn = arg;
json_rpc_cancel(pawn->jrpc);
}

27
src/pawn_lsp.h Normal file
View file

@ -0,0 +1,27 @@
#ifndef _PAWN_LSP_H_
#define _PAWN_LSP_H_
#include "json_rpc.h"
typedef struct pawn_lsp_struct {
/** Active JRPC instance */
json_rpc_t *jrpc;
/** Path to the pawncc compiler */
char *pawncc;
/** User provided args for pawncc */
char **pawn_args;
} pawn_lsp_t;
/**
* @brief Register JSON RPC methods for the Pawn language server
*/
int pawn_lsp_init(pawn_lsp_t *pawn,
json_rpc_t *jrpc,
const char *pawncc,
char **pawn_argv,
int pawn_argc);
void pawn_lsp_deinit(pawn_lsp_t *pawn);
#endif /* _PAWN_LSP_H_ */