diff --git a/Makefile b/Makefile index d21be45..ea9f3bc 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,8 @@ INCLUDES= \ SRCS= \ libraries/cJSON/cJSON.c \ src/json_rpc.c \ - src/main.c + src/main.c \ + src/pawn_lsp.c OBJS=$(addprefix $(BUILD_DIR)/,$(addsuffix .o,$(basename $(SRCS)))) DEPS=$(addprefix $(BUILD_DIR)/,$(addsuffix .d,$(basename $(SRCS)))) diff --git a/src/json_rpc.c b/src/json_rpc.c index a85cb8a..880b8ea 100644 --- a/src/json_rpc.c +++ b/src/json_rpc.c @@ -9,6 +9,7 @@ #include "cJSON/cJSON.h" #include "log.h" +#include "lsp.h" #include "json_rpc.h" 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); } -int -json_rpc_add_method(json_rpc_t *jrpc, const char *method, - int (*callback)(json_rpc_request_t *req, cJSON **response, void *arg), - void *arg) +void +json_rpc_set_request_handler(json_rpc_t *jrpc, + int (*callback)(json_rpc_request_t *req, cJSON **resp, 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 @@ -73,12 +84,6 @@ json_rpc_run(json_rpc_t *jrpc) sched_yield(); } - - free(jrpc->buf); - for (size_t i = 0; i < jrpc->method_cnt; i++) { - free(jrpc->methods[i].method); - } - free(jrpc->methods); } 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: \r\n" */ + if ((rlen > 18) && (!memcmp(&buf[p], "Content-Length: ", 16))) { + (*content_len) = strtoul(&buf[p + 16], NULL, 10); + } + /* "Content-Type: \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 _service_request(json_rpc_t *jrpc) { - char *buf = jrpc->buf; - char *len_str; - char *content; - size_t p = 0; - size_t ret; - long content_len; + char headers[1024] = { 0 }; + size_t content_length = 0; + char *content_type = NULL; + size_t rlen; json_rpc_request_t request; - /* read Content-Length: ...\r\n */ - ret = _read_bytes(jrpc->infd, &buf[p], 16, 1000); - if ((ret != 16) || (memcmp(&buf[p], "Content-Length: ", 16))) { - log_error("Failed to read request header\r\n"); + if (_read_headers(jrpc->infd, headers, sizeof(headers), &content_length, &content_type)) { + log_error("Failed to read/parse headers\r\n"); return; } - p += ret; - len_str = &buf[p]; - - ret = _read_until(jrpc->infd, &buf[p], jrpc->bufsz - p, '\n', 1000); - p += ret; - if ((ret == 0) || (buf[p-1] != '\n')) { - log_error("Failed to read content length value\r\n"); + log_info("Request { length: %ld, type: %s }\r\n", content_length, content_type); + if (content_length == 0) { + log_error("Content-Length of 0\r\n"); + return; + } + else if (content_length > jrpc->bufsz) { + 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; } - content_len = strtol(len_str, NULL, 10); - if (content_len < 0) { - log_warn("Invalid content length %ld\r\n", content_len); - 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) { + memset(jrpc->buf, 0, jrpc->bufsz); + rlen = _read_bytes(jrpc->infd, jrpc->buf, content_length, 1000); + if (rlen != content_length) { log_error("Failed to read all of content, exp %ld, got %ld\r\n", - content_len, ret); + content_length, rlen); return; } - p += ret; - buf[p] = '\0'; /* JSON parsing */ - request.request = cJSON_Parse(content); + request.request = cJSON_Parse(jrpc->buf); 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; } @@ -164,8 +189,9 @@ _service_request(json_rpc_t *jrpc) } request.id = cJSON_GetObjectItem(request.request, "id"); - if ((!request.id) || - ((!cJSON_IsNumber(request.id)) && (!cJSON_IsString(request.id)))) { + if ((request.id != NULL) && + (!cJSON_IsNumber(request.id)) && + (!cJSON_IsString(request.id))) { log_error("Invalid ID format\r\n"); return; } @@ -177,19 +203,19 @@ _service_request(json_rpc_t *jrpc) } request.params = cJSON_GetObjectItem(request.request, "params"); - if (request.params) { - if ((!cJSON_IsArray(request.params)) && (!cJSON_IsObject(request.params))) { - log_error("Invalid params format\r\n"); - return; - } + if ((request.params != NULL) && + (!cJSON_IsArray(request.params)) && + (!cJSON_IsObject(request.params))) { + log_error("Invalid params format\r\n"); + return; } - { + if (request.id) { + /* Request */ cJSON *response = cJSON_CreateObject(); cJSON *val = NULL; cJSON *body = NULL; const char *resp_token = "error"; - json_rpc_method_t *method = NULL; if (response == NULL) { cJSON_Delete(request.request); @@ -207,24 +233,14 @@ _service_request(json_rpc_t *jrpc) return; } - for (size_t i = 0; i < jrpc->method_cnt; i++) { - if (!strcmp(jrpc->methods[i].method, request.method->valuestring)) { - method = &jrpc->methods[i]; - break; - } + if (cJSON_AddStringToObject(response, "jsonrpc", "2.0") == NULL) { + cJSON_Delete(response); + cJSON_Delete(request.request); + return; } - if (!method) { - log_warn("No handler for method \"%s\"\r\n", request.method->valuestring); - } 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 (jrpc->req_handler.callback(&request, &body, jrpc->req_handler.arg) == 0) { + resp_token = "result"; } if (body) { @@ -238,7 +254,7 @@ _service_request(json_rpc_t *jrpc) 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_Delete(body); cJSON_Delete(response); @@ -269,8 +285,10 @@ _service_request(json_rpc_t *jrpc) free(resp_str); } - cJSON_Delete(body); cJSON_Delete(response); + } else { + /* Notify */ + jrpc->notf_handler.callback(&request, jrpc->notf_handler.arg); } cJSON_Delete(request.request); diff --git a/src/json_rpc.h b/src/json_rpc.h index 7496800..8788f45 100644 --- a/src/json_rpc.h +++ b/src/json_rpc.h @@ -9,7 +9,8 @@ typedef struct json_rpc_request_struct { /** Base request structure, guaranteed to be an object */ 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; /** Reference to the value of the method field, guaranteed to be a string */ cJSON *method; @@ -18,26 +19,21 @@ typedef struct json_rpc_request_struct { cJSON *params; } 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 { /** Socket to read requests from */ int infd; /** Socket to write responses to */ int outfd; - /** List of methods added */ - json_rpc_method_t *methods; - /** Number of methods added */ - size_t method_cnt; + /** Registered request handler info */ + struct { + int (*callback)(json_rpc_request_t *, cJSON **, void *); + void *arg; + } req_handler; + /** Registered notification handler info */ + struct { + void (*callback)(json_rpc_request_t *, void *); + void *arg; + } notf_handler; /** Main server thread */ pthread_t thread; /** 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_add_method(json_rpc_t *jrpc, const char *method, - int (*callback)(json_rpc_request_t *req, cJSON **response, void *arg), - void *arg); +void json_rpc_set_request_handler(json_rpc_t *jrpc, + int (*callback)(json_rpc_request_t *req, cJSON **resp, 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); diff --git a/src/lsp.h b/src/lsp.h index afb8dd2..2a87184 100644 --- a/src/lsp.h +++ b/src/lsp.h @@ -1,10 +1,6 @@ #ifndef _LSP_H_ #define _LSP_H_ -#include -#include -#include - /* Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ */ typedef enum lsp_error_enum { diff --git a/src/main.c b/src/main.c index 3ad7d88..d817f3b 100644 --- a/src/main.c +++ b/src/main.c @@ -1,14 +1,17 @@ #include +#include #include #include #include #include "log.h" #include "json_rpc.h" +#include "pawn_lsp.h" void sighandler(int signal); static json_rpc_t jrpc; +static pawn_lsp_t pawn; FILE *logger = NULL; @@ -42,11 +45,18 @@ main(int argc, char **argv) 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); + pawn_lsp_deinit(&pawn); if (logger) { fclose(logger); } + tcsetattr(STDIN_FILENO, TCSAFLUSH, &term_orig); return (0); } diff --git a/src/pawn_lsp.c b/src/pawn_lsp.c new file mode 100644 index 0000000..1739a6f --- /dev/null +++ b/src/pawn_lsp.c @@ -0,0 +1,132 @@ +#include +#include + +#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 -d0 -v=2 -o/tmp/... [...args] )*/ + pawn->pawn_args = calloc(pawn_argc + 5, sizeof(char *)); + /* pawn->pawn_args[0] = , 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); +} + diff --git a/src/pawn_lsp.h b/src/pawn_lsp.h new file mode 100644 index 0000000..e833a38 --- /dev/null +++ b/src/pawn_lsp.h @@ -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_ */ +