diff options
author | Eliseo Martínez <eliseomarmol@gmail.com> | 2014-05-12 02:25:17 +0200 |
---|---|---|
committer | Eliseo Martínez <eliseomarmol@gmail.com> | 2014-05-15 20:46:01 +0200 |
commit | da51dc9cf202772f60bd2da975dbef257bd9237c (patch) | |
tree | 5c16b93238a153f55634e9323077f30c8133970c /src/nvim/os | |
parent | ffe61e5ba1721340ca51d56bae3ddaca415fb5bc (diff) | |
download | rneovim-da51dc9cf202772f60bd2da975dbef257bd9237c.tar.gz rneovim-da51dc9cf202772f60bd2da975dbef257bd9237c.tar.bz2 rneovim-da51dc9cf202772f60bd2da975dbef257bd9237c.zip |
Introduce nvim namespace: Move files.
Move files from src/ to src/nvim/.
- src/nvim/ becomes the new root dir for nvim executable sources.
- src/libnvim/ is planned to become root dir of the neovim library.
Diffstat (limited to 'src/nvim/os')
35 files changed, 3962 insertions, 0 deletions
diff --git a/src/nvim/os/channel.c b/src/nvim/os/channel.c new file mode 100644 index 0000000000..2427db2a14 --- /dev/null +++ b/src/nvim/os/channel.c @@ -0,0 +1,175 @@ +#include <string.h> + +#include <uv.h> +#include <msgpack.h> + +#include "lib/klist.h" +#include "os/channel.h" +#include "os/channel_defs.h" +#include "os/rstream.h" +#include "os/rstream_defs.h" +#include "os/wstream.h" +#include "os/wstream_defs.h" +#include "os/job.h" +#include "os/job_defs.h" +#include "os/msgpack_rpc.h" +#include "vim.h" +#include "memory.h" + +typedef struct { + ChannelProtocol protocol; + bool is_job; + union { + struct { + msgpack_unpacker *unpacker; + msgpack_sbuffer *sbuffer; + } msgpack; + } proto; + union { + int job_id; + struct { + RStream *read; + WStream *write; + } streams; + } data; +} Channel; + +#define _destroy_channel(x) + +KLIST_INIT(Channel, Channel *, _destroy_channel) + +static klist_t(Channel) *channels = NULL; +static void on_job_stdout(RStream *rstream, void *data, bool eof); +static void on_job_stderr(RStream *rstream, void *data, bool eof); +static void parse_msgpack(RStream *rstream, void *data, bool eof); + +void channel_init() +{ + channels = kl_init(Channel); +} + +void channel_teardown() +{ + if (!channels) { + return; + } + + Channel *channel; + + while (kl_shift(Channel, channels, &channel) == 0) { + + switch (channel->protocol) { + case kChannelProtocolMsgpack: + msgpack_sbuffer_free(channel->proto.msgpack.sbuffer); + msgpack_unpacker_free(channel->proto.msgpack.unpacker); + break; + default: + abort(); + } + + if (channel->is_job) { + job_stop(channel->data.job_id); + } else { + rstream_free(channel->data.streams.read); + wstream_free(channel->data.streams.write); + } + } +} + +void channel_from_job(char **argv, ChannelProtocol prot) +{ + Channel *channel = xmalloc(sizeof(Channel)); + rstream_cb rcb = NULL; + + switch (prot) { + case kChannelProtocolMsgpack: + rcb = on_job_stdout; + channel->proto.msgpack.unpacker = + msgpack_unpacker_new(MSGPACK_UNPACKER_INIT_BUFFER_SIZE); + channel->proto.msgpack.sbuffer = msgpack_sbuffer_new(); + break; + default: + abort(); + } + + channel->protocol = prot; + channel->is_job = true; + channel->data.job_id = job_start(argv, channel, rcb, on_job_stderr, NULL); + *kl_pushp(Channel, channels) = channel; +} + +void channel_from_stream(uv_stream_t *stream, ChannelProtocol prot) +{ + Channel *channel = xmalloc(sizeof(Channel)); + rstream_cb rcb = NULL; + + switch (prot) { + case kChannelProtocolMsgpack: + rcb = parse_msgpack; + channel->proto.msgpack.unpacker = + msgpack_unpacker_new(MSGPACK_UNPACKER_INIT_BUFFER_SIZE); + channel->proto.msgpack.sbuffer = msgpack_sbuffer_new(); + break; + default: + abort(); + } + + stream->data = NULL; + channel->protocol = prot; + channel->is_job = false; + // read stream + channel->data.streams.read = rstream_new(rcb, 1024, channel, true); + rstream_set_stream(channel->data.streams.read, stream); + rstream_start(channel->data.streams.read); + // write stream + channel->data.streams.write = wstream_new(1024 * 1024); + wstream_set_stream(channel->data.streams.write, stream); + // push to channel list + *kl_pushp(Channel, channels) = channel; +} + +static void on_job_stdout(RStream *rstream, void *data, bool eof) +{ + Job *job = data; + parse_msgpack(rstream, job_data(job), eof); +} + +static void on_job_stderr(RStream *rstream, void *data, bool eof) +{ + // TODO(tarruda): plugin error messages should be sent to the error buffer +} + +static void parse_msgpack(RStream *rstream, void *data, bool eof) +{ + msgpack_unpacked unpacked; + Channel *channel = data; + uint32_t count = rstream_available(rstream); + + // Feed the unpacker with data + msgpack_unpacker_reserve_buffer(channel->proto.msgpack.unpacker, count); + rstream_read(rstream, + msgpack_unpacker_buffer(channel->proto.msgpack.unpacker), + count); + msgpack_unpacker_buffer_consumed(channel->proto.msgpack.unpacker, count); + + msgpack_unpacked_init(&unpacked); + + // Deserialize everything we can. + while (msgpack_unpacker_next(channel->proto.msgpack.unpacker, &unpacked)) { + // Each object is a new msgpack-rpc request and requires an empty response + msgpack_packer response; + msgpack_packer_init(&response, + channel->proto.msgpack.sbuffer, + msgpack_sbuffer_write); + // Perform the call + msgpack_rpc_call(&unpacked.data, &response); + wstream_write(channel->data.streams.write, + xmemdup(channel->proto.msgpack.sbuffer->data, + channel->proto.msgpack.sbuffer->size), + channel->proto.msgpack.sbuffer->size, + true); + + // Clear the buffer for future calls + msgpack_sbuffer_clear(channel->proto.msgpack.sbuffer); + } +} diff --git a/src/nvim/os/channel.h b/src/nvim/os/channel.h new file mode 100644 index 0000000000..c58e91a0e5 --- /dev/null +++ b/src/nvim/os/channel.h @@ -0,0 +1,29 @@ +#ifndef NEOVIM_OS_CHANNEL_H +#define NEOVIM_OS_CHANNEL_H + +#include <uv.h> + +#include "os/channel_defs.h" + +/// Initializes the module +void channel_init(void); + +/// Teardown the module +void channel_teardown(void); + +/// Creates an API channel from a libuv stream representing a tcp or +/// pipe/socket client connection +/// +/// @param stream The established connection +/// @param prot The rpc protocol used +void channel_from_stream(uv_stream_t *stream, ChannelProtocol prot); + +/// Creates an API channel by starting a job and connecting to its +/// stdin/stdout. stderr is forwarded to the editor error stream. +/// +/// @param argv The argument vector for the process +/// @param prot The rpc protocol used +void channel_from_job(char **argv, ChannelProtocol prot); + +#endif // NEOVIM_OS_CHANNEL_H + diff --git a/src/nvim/os/channel_defs.h b/src/nvim/os/channel_defs.h new file mode 100644 index 0000000000..27ba103fbe --- /dev/null +++ b/src/nvim/os/channel_defs.h @@ -0,0 +1,8 @@ +#ifndef NEOVIM_OS_CHANNEL_DEFS_H +#define NEOVIM_OS_CHANNEL_DEFS_H + +typedef enum { + kChannelProtocolMsgpack +} ChannelProtocol; + +#endif // NEOVIM_OS_CHANNEL_DEFS_H diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c new file mode 100644 index 0000000000..e6cdb92ea8 --- /dev/null +++ b/src/nvim/os/env.c @@ -0,0 +1,80 @@ +// env.c -- environment variable access + +#include <uv.h> + +#include "os/os.h" +#include "misc2.h" + +#ifdef HAVE__NSGETENVIRON +#include <crt_externs.h> +#endif + +#ifdef HAVE_SYS_UTSNAME_H +#include <sys/utsname.h> +#endif + +const char *os_getenv(const char *name) +{ + return getenv(name); +} + +int os_setenv(const char *name, const char *value, int overwrite) +{ + return setenv(name, value, overwrite); +} + +char *os_getenvname_at_index(size_t index) +{ +# if defined(HAVE__NSGETENVIRON) + char **environ = *_NSGetEnviron(); +# elif !defined(__WIN32__) + // Borland C++ 5.2 has this in a header file. + extern char **environ; +# endif + // check if index is inside the environ array + for (size_t i = 0; i < index; i++) { + if (environ[i] == NULL) { + return NULL; + } + } + char *str = environ[index]; + if (str == NULL) { + return NULL; + } + int namesize = 0; + while (str[namesize] != '=' && str[namesize] != NUL) { + namesize++; + } + char *name = (char *)vim_strnsave((char_u *)str, namesize); + return name; +} + + +int64_t os_get_pid() +{ +#ifdef _WIN32 + return (int64_t)GetCurrentProcessId(); +#else + return (int64_t)getpid(); +#endif +} + +void os_get_hostname(char *hostname, size_t len) +{ +#ifdef HAVE_SYS_UTSNAME_H + struct utsname vutsname; + + if (uname(&vutsname) < 0) { + *hostname = '\0'; + } else { + strncpy(hostname, vutsname.nodename, len - 1); + hostname[len - 1] = '\0'; + } +#else + // TODO(unknown): Implement this for windows. + // See the implementation used in vim: + // https://code.google.com/p/vim/source/browse/src/os_win32.c?r=6b69d8dde19e32909f4ee3a6337e6a2ecfbb6f72#2899 + *hostname = '\0'; +#endif +} + diff --git a/src/nvim/os/event.c b/src/nvim/os/event.c new file mode 100644 index 0000000000..2c25852778 --- /dev/null +++ b/src/nvim/os/event.c @@ -0,0 +1,148 @@ +#include <stdint.h> +#include <stdbool.h> +#include <stdlib.h> + +#include <uv.h> + +#include "lib/klist.h" +#include "os/event.h" +#include "os/input.h" +#include "os/channel.h" +#include "os/server.h" +#include "os/signal.h" +#include "os/rstream.h" +#include "os/job.h" +#include "vim.h" +#include "memory.h" +#include "misc2.h" + +// event will be cleaned up after it gets processed +#define _destroy_event(x) // do nothing +KLIST_INIT(Event, Event, _destroy_event) + +static klist_t(Event) *event_queue; +static uv_timer_t timer; +static uv_prepare_t timer_prepare; +static void timer_cb(uv_timer_t *handle); +static void timer_prepare_cb(uv_prepare_t *); + +void event_init() +{ + // Initialize the event queue + event_queue = kl_init(Event); + // Initialize input events + input_init(); + // Timer to wake the event loop if a timeout argument is passed to + // `event_poll` + // Signals + signal_init(); + // Jobs + job_init(); + // Channels + channel_init(); + // Servers + server_init(); + uv_timer_init(uv_default_loop(), &timer); + // This prepare handle that actually starts the timer + uv_prepare_init(uv_default_loop(), &timer_prepare); +} + +void event_teardown() +{ + channel_teardown(); + job_teardown(); + server_teardown(); +} + +// Wait for some event +bool event_poll(int32_t ms) +{ + bool timed_out; + uv_run_mode run_mode = UV_RUN_ONCE; + + if (input_ready()) { + // If there's a pending input event to be consumed, do it now + return true; + } + + input_start(); + timed_out = false; + + if (ms > 0) { + // Timeout passed as argument to the timer + timer.data = &timed_out; + // We only start the timer after the loop is running, for that we + // use an prepare handle(pass the interval as data to it) + timer_prepare.data = &ms; + uv_prepare_start(&timer_prepare, timer_prepare_cb); + } else if (ms == 0) { + // For ms == 0, we need to do a non-blocking event poll by + // setting the run mode to UV_RUN_NOWAIT. + run_mode = UV_RUN_NOWAIT; + } + + do { + // Run one event loop iteration, blocking for events if run_mode is + // UV_RUN_ONCE + uv_run(uv_default_loop(), run_mode); + } while ( + // Continue running if ... + !input_ready() && // we have no input + kl_empty(event_queue) && // no events are waiting to be processed + run_mode != UV_RUN_NOWAIT && // ms != 0 + !timed_out); // we didn't get a timeout + + input_stop(); + + if (ms > 0) { + // Stop the timer + uv_timer_stop(&timer); + } + + return input_ready() || event_is_pending(); +} + +bool event_is_pending() +{ + return !kl_empty(event_queue); +} + +// Push an event to the queue +void event_push(Event event) +{ + *kl_pushp(Event, event_queue) = event; +} + +// Runs the appropriate action for each queued event +void event_process() +{ + Event event; + + while (kl_shift(Event, event_queue, &event) == 0) { + switch (event.type) { + case kEventSignal: + signal_handle(event); + break; + case kEventRStreamData: + rstream_read_event(event); + break; + case kEventJobExit: + job_exit_event(event); + break; + default: + abort(); + } + } +} + +// Set a flag in the `event_poll` loop for signaling of a timeout +static void timer_cb(uv_timer_t *handle) +{ + *((bool *)handle->data) = true; +} + +static void timer_prepare_cb(uv_prepare_t *handle) +{ + uv_timer_start(&timer, timer_cb, *(uint32_t *)timer_prepare.data, 0); + uv_prepare_stop(&timer_prepare); +} diff --git a/src/nvim/os/event.h b/src/nvim/os/event.h new file mode 100644 index 0000000000..5ddca46875 --- /dev/null +++ b/src/nvim/os/event.h @@ -0,0 +1,18 @@ +#ifndef NEOVIM_OS_EVENT_H +#define NEOVIM_OS_EVENT_H + +#include <stdint.h> +#include <stdbool.h> + +#include "os/event_defs.h" +#include "os/job_defs.h" + +void event_init(void); +void event_teardown(void); +bool event_poll(int32_t ms); +bool event_is_pending(void); +void event_push(Event event); +void event_process(void); + +#endif // NEOVIM_OS_EVENT_H + diff --git a/src/nvim/os/event_defs.h b/src/nvim/os/event_defs.h new file mode 100644 index 0000000000..428ae50f3e --- /dev/null +++ b/src/nvim/os/event_defs.h @@ -0,0 +1,25 @@ +#ifndef NEOVIM_OS_EVENT_DEFS_H +#define NEOVIM_OS_EVENT_DEFS_H + +#include "os/job_defs.h" +#include "os/rstream_defs.h" + +typedef enum { + kEventSignal, + kEventRStreamData, + kEventJobExit +} EventType; + +typedef struct { + EventType type; + union { + int signum; + struct { + RStream *ptr; + bool eof; + } rstream; + Job *job; + } data; +} Event; + +#endif // NEOVIM_OS_EVENT_DEFS_H diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c new file mode 100644 index 0000000000..89eb4c8691 --- /dev/null +++ b/src/nvim/os/fs.c @@ -0,0 +1,277 @@ +// fs.c -- filesystem access + +#include "os/os.h" +#include "memory.h" +#include "message.h" +#include "misc1.h" +#include "misc2.h" +#include "path.h" + +static bool is_executable(const char_u *name); +static bool is_executable_in_path(const char_u *name); + +// Many fs functions from libuv return that value on success. +static const int kLibuvSuccess = 0; + +int os_chdir(const char *path) { + if (p_verbose >= 5) { + verbose_enter(); + smsg((char_u *)"chdir(%s)", path); + verbose_leave(); + } + return uv_chdir(path); +} + +int os_dirname(char_u *buf, size_t len) +{ + assert(buf && len); + + int errno; + if ((errno = uv_cwd((char *)buf, &len)) != kLibuvSuccess) { + vim_strncpy(buf, (char_u *)uv_strerror(errno), len - 1); + return FAIL; + } + return OK; +} + +bool os_isdir(const char_u *name) +{ + int32_t mode = os_getperm(name); + if (mode < 0) { + return false; + } + + if (!S_ISDIR(mode)) { + return false; + } + + return true; +} + +bool os_can_exe(const char_u *name) +{ + // If it's an absolute or relative path don't need to use $PATH. + if (path_is_absolute_path(name) || + (name[0] == '.' && (name[1] == '/' || + (name[1] == '.' && name[2] == '/')))) { + return is_executable(name); + } + + return is_executable_in_path(name); +} + +// Return true if "name" is an executable file, false if not or it doesn't +// exist. +static bool is_executable(const char_u *name) +{ + int32_t mode = os_getperm(name); + + if (mode < 0) { + return false; + } + + if (S_ISREG(mode) && (S_IEXEC & mode)) { + return true; + } + + return false; +} + +/// Check if a file is inside the $PATH and is executable. +/// +/// @return `true` if `name` is an executable inside $PATH. +static bool is_executable_in_path(const char_u *name) +{ + const char *path = getenv("PATH"); + // PATH environment variable does not exist or is empty. + if (path == NULL || *path == NUL) { + return false; + } + + int buf_len = STRLEN(name) + STRLEN(path) + 2; + char_u *buf = alloc((unsigned)(buf_len)); + + // Walk through all entries in $PATH to check if "name" exists there and + // is an executable file. + for (;; ) { + const char *e = strchr(path, ':'); + if (e == NULL) { + e = path + STRLEN(path); + } + + // Glue together the given directory from $PATH with name and save into + // buf. + vim_strncpy(buf, (char_u *) path, e - path); + append_path((char *) buf, (const char *) name, buf_len); + + if (is_executable(buf)) { + // Found our executable. Free buf and return. + free(buf); + return true; + } + + if (*e != ':') { + // End of $PATH without finding any executable called name. + free(buf); + return false; + } + + path = e + 1; + } + + // We should never get to this point. + assert(false); + return false; +} + +int os_stat(const char_u *name, uv_stat_t *statbuf) +{ + uv_fs_t request; + int result = uv_fs_stat(uv_default_loop(), &request, + (const char *)name, NULL); + *statbuf = request.statbuf; + uv_fs_req_cleanup(&request); + + if (result == kLibuvSuccess) { + return OK; + } + + return FAIL; +} + +int32_t os_getperm(const char_u *name) +{ + uv_stat_t statbuf; + if (os_stat(name, &statbuf) == FAIL) { + return -1; + } else { + return (int32_t)statbuf.st_mode; + } +} + +int os_setperm(const char_u *name, int perm) +{ + uv_fs_t request; + int result = uv_fs_chmod(uv_default_loop(), &request, + (const char*)name, perm, NULL); + uv_fs_req_cleanup(&request); + + if (result == kLibuvSuccess) { + return OK; + } + + return FAIL; +} + +bool os_file_exists(const char_u *name) +{ + uv_stat_t statbuf; + if (os_stat(name, &statbuf) == OK) { + return true; + } + + return false; +} + +bool os_file_is_readonly(const char *name) +{ + return access(name, W_OK) != 0; +} + +int os_file_is_writable(const char *name) +{ + if (access(name, W_OK) == 0) { + if (os_isdir((char_u *)name)) { + return 2; + } + return 1; + } + return 0; +} + +bool os_get_file_size(const char *name, off_t *size) +{ + uv_stat_t statbuf; + if (os_stat((char_u *)name, &statbuf) == OK) { + *size = statbuf.st_size; + return true; + } + return false; +} + +int os_rename(const char_u *path, const char_u *new_path) +{ + uv_fs_t request; + int result = uv_fs_rename(uv_default_loop(), &request, + (const char *)path, (const char *)new_path, NULL); + uv_fs_req_cleanup(&request); + + if (result == kLibuvSuccess) { + return OK; + } + + return FAIL; +} + +int os_mkdir(const char *path, int32_t mode) +{ + uv_fs_t request; + int result = uv_fs_mkdir(uv_default_loop(), &request, path, mode, NULL); + uv_fs_req_cleanup(&request); + return result; +} + +int os_rmdir(const char *path) +{ + uv_fs_t request; + int result = uv_fs_rmdir(uv_default_loop(), &request, path, NULL); + uv_fs_req_cleanup(&request); + return result; +} + +int os_remove(const char *path) +{ + uv_fs_t request; + int result = uv_fs_unlink(uv_default_loop(), &request, path, NULL); + uv_fs_req_cleanup(&request); + return result; +} + +bool os_get_file_info(const char *path, FileInfo *file_info) +{ + if (os_stat((char_u *)path, &(file_info->stat)) == OK) { + return true; + } + return false; +} + +bool os_get_file_info_link(const char *path, FileInfo *file_info) +{ + uv_fs_t request; + int result = uv_fs_lstat(uv_default_loop(), &request, path, NULL); + file_info->stat = request.statbuf; + uv_fs_req_cleanup(&request); + if (result == kLibuvSuccess) { + return true; + } + return false; +} + +bool os_get_file_info_fd(int file_descriptor, FileInfo *file_info) +{ + uv_fs_t request; + int result = uv_fs_fstat(uv_default_loop(), &request, file_descriptor, NULL); + file_info->stat = request.statbuf; + uv_fs_req_cleanup(&request); + if (result == kLibuvSuccess) { + return true; + } + return false; +} + +bool os_file_info_id_equal(FileInfo *file_info_1, FileInfo *file_info_2) +{ + return file_info_1->stat.st_ino == file_info_2->stat.st_ino + && file_info_1->stat.st_dev == file_info_2->stat.st_dev; +} + diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c new file mode 100644 index 0000000000..4310edd514 --- /dev/null +++ b/src/nvim/os/input.c @@ -0,0 +1,195 @@ +#include <string.h> +#include <stdint.h> +#include <stdbool.h> + +#include <uv.h> + +#include "os/input.h" +#include "os/event.h" +#include "os/rstream_defs.h" +#include "os/rstream.h" +#include "vim.h" +#include "ui.h" +#include "fileio.h" +#include "getchar.h" +#include "term.h" + +#define READ_BUFFER_SIZE 256 + +typedef enum { + kInputNone, + kInputAvail, + kInputEof +} InbufPollResult; + +static RStream *read_stream; +static bool eof = false, started_reading = false; + +static InbufPollResult inbuf_poll(int32_t ms); +static void stderr_switch(void); +static void read_cb(RStream *rstream, void *data, bool eof); +// Helper function used to push bytes from the 'event' key sequence partially +// between calls to os_inchar when maxlen < 3 +static int push_event_key(uint8_t *buf, int maxlen); + +void input_init() +{ + read_stream = rstream_new(read_cb, READ_BUFFER_SIZE, NULL, false); + rstream_set_file(read_stream, read_cmd_fd); +} + +// Check if there's pending input +bool input_ready() +{ + return rstream_available(read_stream) > 0 || eof; +} + +// Listen for input +void input_start() +{ + rstream_start(read_stream); +} + +// Stop listening for input +void input_stop() +{ + rstream_stop(read_stream); +} + +// Copies (at most `count`) of was read from `read_cmd_fd` into `buf` +uint32_t input_read(char *buf, uint32_t count) +{ + return rstream_read(read_stream, buf, count); +} + + +// Low level input function. +int os_inchar(uint8_t *buf, int maxlen, int32_t ms, int tb_change_cnt) +{ + InbufPollResult result; + + if (event_is_pending()) { + // Return pending event bytes + return push_event_key(buf, maxlen); + } + + if (ms >= 0) { + if ((result = inbuf_poll(ms)) == kInputNone) { + return 0; + } + } else { + if ((result = inbuf_poll(p_ut)) == kInputNone) { + if (trigger_cursorhold() && maxlen >= 3 + && !typebuf_changed(tb_change_cnt)) { + buf[0] = K_SPECIAL; + buf[1] = KS_EXTRA; + buf[2] = KE_CURSORHOLD; + return 3; + } + + before_blocking(); + result = inbuf_poll(-1); + } + } + + // If there are pending events, return the keys directly + if (event_is_pending()) { + return push_event_key(buf, maxlen); + } + + // If input was put directly in typeahead buffer bail out here. + if (typebuf_changed(tb_change_cnt)) { + return 0; + } + + if (result == kInputEof) { + read_error_exit(); + return 0; + } + + return read_from_input_buf(buf, (int64_t)maxlen); +} + +// Check if a character is available for reading +bool os_char_avail() +{ + return inbuf_poll(0) == kInputAvail; +} + +// Check for CTRL-C typed by reading all available characters. +// In cooked mode we should get SIGINT, no need to check. +void os_breakcheck() +{ + if (curr_tmode == TMODE_RAW && event_poll(0)) + fill_input_buf(false); +} + +bool os_isatty(int fd) +{ + return uv_guess_handle(fd) == UV_TTY; +} + +// This is a replacement for the old `WaitForChar` function in os_unix.c +static InbufPollResult inbuf_poll(int32_t ms) +{ + if (input_available()) { + return kInputAvail; + } + + if (event_poll(ms)) { + return eof && rstream_available(read_stream) == 0 ? + kInputEof : + kInputAvail; + } + + return kInputNone; +} + +static void stderr_switch() +{ + int mode = cur_tmode; + // We probably set the wrong file descriptor to raw mode. Switch back to + // cooked mode + settmode(TMODE_COOK); + // Stop the idle handle + rstream_stop(read_stream); + // Use stderr for stdin, also works for shell commands. + read_cmd_fd = 2; + // Initialize and start the input stream + rstream_set_file(read_stream, read_cmd_fd); + rstream_start(read_stream); + // Set the mode back to what it was + settmode(mode); +} + +static void read_cb(RStream *rstream, void *data, bool at_eof) +{ + if (at_eof) { + if (!started_reading + && rstream_is_regular_file(rstream) + && os_isatty(STDERR_FILENO)) { + // Read error. Since stderr is a tty we switch to reading from it. This + // is for handling for cases like "foo | xargs vim" because xargs + // redirects stdin from /dev/null. Previously, this was done in ui.c + stderr_switch(); + } else { + eof = true; + } + } + + started_reading = true; +} + +static int push_event_key(uint8_t *buf, int maxlen) +{ + static const uint8_t key[3] = { K_SPECIAL, KS_EXTRA, KE_EVENT }; + static int key_idx = 0; + int buf_idx = 0; + + do { + buf[buf_idx++] = key[key_idx++]; + key_idx %= 3; + } while (key_idx > 0 && buf_idx < maxlen); + + return buf_idx; +} diff --git a/src/nvim/os/input.h b/src/nvim/os/input.h new file mode 100644 index 0000000000..9ffd50fd3f --- /dev/null +++ b/src/nvim/os/input.h @@ -0,0 +1,23 @@ +#ifndef NEOVIM_OS_INPUT_H +#define NEOVIM_OS_INPUT_H + +#include <stdint.h> +#include <stdbool.h> + +void input_init(void); +bool input_ready(void); +void input_start(void); +void input_stop(void); +uint32_t input_read(char *buf, uint32_t count); +int os_inchar(uint8_t *, int, int32_t, int); +bool os_char_avail(void); +void os_breakcheck(void); + +/// Test whether a file descriptor refers to a terminal. +/// +/// @param fd File descriptor. +/// @return `true` if file descriptor refers to a terminal. +bool os_isatty(int fd); + +#endif // NEOVIM_OS_INPUT_H + diff --git a/src/nvim/os/job.c b/src/nvim/os/job.c new file mode 100644 index 0000000000..7940338b70 --- /dev/null +++ b/src/nvim/os/job.c @@ -0,0 +1,370 @@ +#include <stdint.h> +#include <stdbool.h> + +#include <uv.h> + +#include "os/uv_helpers.h" +#include "os/job.h" +#include "os/job_defs.h" +#include "os/rstream.h" +#include "os/rstream_defs.h" +#include "os/wstream.h" +#include "os/wstream_defs.h" +#include "os/event.h" +#include "os/event_defs.h" +#include "os/time.h" +#include "os/shell.h" +#include "vim.h" +#include "memory.h" +#include "term.h" + +#define EXIT_TIMEOUT 25 +#define MAX_RUNNING_JOBS 100 +#define JOB_BUFFER_SIZE 1024 +#define JOB_WRITE_MAXMEM 1024 * 1024 + +struct job { + // Job id the index in the job table plus one. + int id; + // Number of polls after a SIGTERM that will trigger a SIGKILL + int exit_timeout; + // exit_cb may be called while there's still pending data from stdout/stderr. + // We use this reference count to ensure the JobExit event is only emitted + // when stdout/stderr are drained + int pending_refs; + // Same as above, but for freeing the job memory which contains + // libuv handles. Only after all are closed the job can be safely freed. + int pending_closes; + // If the job was already stopped + bool stopped; + // Data associated with the job + void *data; + // Callbacks + job_exit_cb exit_cb; + rstream_cb stdout_cb, stderr_cb; + // Readable streams(std{out,err}) + RStream *out, *err; + // Writable stream(stdin) + WStream *in; + // Structures for process spawning/management used by libuv + uv_process_t proc; + uv_process_options_t proc_opts; + uv_stdio_container_t stdio[3]; + uv_pipe_t proc_stdin, proc_stdout, proc_stderr; +}; + +static Job *table[MAX_RUNNING_JOBS] = {NULL}; +static uint32_t job_count = 0; +static uv_prepare_t job_prepare; + +// Some helpers shared in this module +static bool is_alive(Job *job); +static Job * find_job(int id); +static void free_job(Job *job); + +// Callbacks for libuv +static void job_prepare_cb(uv_prepare_t *handle); +static void read_cb(RStream *rstream, void *data, bool eof); +static void exit_cb(uv_process_t *proc, int64_t status, int term_signal); +static void close_cb(uv_handle_t *handle); +static void emit_exit_event(Job *job); + +void job_init() +{ + uv_disable_stdio_inheritance(); + uv_prepare_init(uv_default_loop(), &job_prepare); +} + +void job_teardown() +{ + // 20 tries will give processes about 1 sec to exit cleanly + uint32_t remaining_tries = 20; + bool all_dead = true; + int i; + Job *job; + + // Politely ask each job to terminate + for (i = 0; i < MAX_RUNNING_JOBS; i++) { + if ((job = table[i]) != NULL) { + all_dead = false; + uv_process_kill(&job->proc, SIGTERM); + } + } + + if (all_dead) { + return; + } + + os_delay(10, 0); + // Right now any exited process are zombies waiting for us to acknowledge + // their status with `wait` or handling SIGCHLD. libuv does that + // automatically (and then calls `exit_cb`) but we have to give it a chance + // by running the loop one more time + uv_run(uv_default_loop(), UV_RUN_NOWAIT); + + // Prepare to start shooting + for (i = 0; i < MAX_RUNNING_JOBS; i++) { + if ((job = table[i]) == NULL) { + continue; + } + + // Still alive + while (is_alive(job) && remaining_tries--) { + os_delay(50, 0); + // Acknowledge child exits + uv_run(uv_default_loop(), UV_RUN_NOWAIT); + } + + if (is_alive(job)) { + uv_process_kill(&job->proc, SIGKILL); + } + } +} + +int job_start(char **argv, + void *data, + rstream_cb stdout_cb, + rstream_cb stderr_cb, + job_exit_cb job_exit_cb) +{ + int i; + Job *job; + + // Search for a free slot in the table + for (i = 0; i < MAX_RUNNING_JOBS; i++) { + if (table[i] == NULL) { + break; + } + } + + if (i == MAX_RUNNING_JOBS) { + // No free slots + return 0; + } + + job = xmalloc(sizeof(Job)); + // Initialize + job->id = i + 1; + job->pending_refs = 3; + job->pending_closes = 4; + job->data = data; + job->stdout_cb = stdout_cb; + job->stderr_cb = stderr_cb; + job->exit_cb = job_exit_cb; + job->stopped = false; + job->exit_timeout = EXIT_TIMEOUT; + job->proc_opts.file = argv[0]; + job->proc_opts.args = argv; + job->proc_opts.stdio = job->stdio; + job->proc_opts.stdio_count = 3; + job->proc_opts.flags = UV_PROCESS_WINDOWS_HIDE; + job->proc_opts.exit_cb = exit_cb; + job->proc_opts.cwd = NULL; + job->proc_opts.env = NULL; + job->proc.data = NULL; + job->proc_stdin.data = NULL; + job->proc_stdout.data = NULL; + job->proc_stderr.data = NULL; + + // Initialize the job std{in,out,err} + uv_pipe_init(uv_default_loop(), &job->proc_stdin, 0); + job->stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; + job->stdio[0].data.stream = (uv_stream_t *)&job->proc_stdin; + + uv_pipe_init(uv_default_loop(), &job->proc_stdout, 0); + job->stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + job->stdio[1].data.stream = (uv_stream_t *)&job->proc_stdout; + + uv_pipe_init(uv_default_loop(), &job->proc_stderr, 0); + job->stdio[2].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + job->stdio[2].data.stream = (uv_stream_t *)&job->proc_stderr; + + // Spawn the job + if (uv_spawn(uv_default_loop(), &job->proc, &job->proc_opts) != 0) { + free_job(job); + return -1; + } + + // Give all handles a reference to the job + handle_set_job((uv_handle_t *)&job->proc, job); + handle_set_job((uv_handle_t *)&job->proc_stdin, job); + handle_set_job((uv_handle_t *)&job->proc_stdout, job); + handle_set_job((uv_handle_t *)&job->proc_stderr, job); + + job->in = wstream_new(JOB_WRITE_MAXMEM); + wstream_set_stream(job->in, (uv_stream_t *)&job->proc_stdin); + // Start the readable streams + job->out = rstream_new(read_cb, JOB_BUFFER_SIZE, job, true); + job->err = rstream_new(read_cb, JOB_BUFFER_SIZE, job, true); + rstream_set_stream(job->out, (uv_stream_t *)&job->proc_stdout); + rstream_set_stream(job->err, (uv_stream_t *)&job->proc_stderr); + rstream_start(job->out); + rstream_start(job->err); + // Save the job to the table + table[i] = job; + + // Start polling job status if this is the first + if (job_count == 0) { + uv_prepare_start(&job_prepare, job_prepare_cb); + } + job_count++; + + return job->id; +} + +bool job_stop(int id) +{ + Job *job = find_job(id); + + if (job == NULL || job->stopped) { + return false; + } + + job->stopped = true; + + return true; +} + +bool job_write(int id, char *data, uint32_t len) +{ + Job *job = find_job(id); + + if (job == NULL || job->stopped) { + free(data); + return false; + } + + if (!wstream_write(job->in, data, len, true)) { + job_stop(job->id); + return false; + } + + return true; +} + +void job_exit_event(Event event) +{ + Job *job = event.data.job; + + // Free the slot now, 'exit_cb' may want to start another job to replace + // this one + table[job->id - 1] = NULL; + + if (job->exit_cb) { + // Invoke the exit callback + job->exit_cb(job, job->data); + } + + // Free the job resources + free_job(job); + + // Stop polling job status if this was the last + job_count--; + if (job_count == 0) { + uv_prepare_stop(&job_prepare); + } +} + +int job_id(Job *job) +{ + return job->id; +} + +void *job_data(Job *job) +{ + return job->data; +} + +static bool is_alive(Job *job) +{ + return uv_process_kill(&job->proc, 0) == 0; +} + +static Job * find_job(int id) +{ + if (id <= 0 || id > MAX_RUNNING_JOBS) { + return NULL; + } + + return table[id - 1]; +} + +static void free_job(Job *job) +{ + uv_close((uv_handle_t *)&job->proc_stdout, close_cb); + uv_close((uv_handle_t *)&job->proc_stdin, close_cb); + uv_close((uv_handle_t *)&job->proc_stderr, close_cb); + uv_close((uv_handle_t *)&job->proc, close_cb); +} + +/// Iterates the table, sending SIGTERM to stopped jobs and SIGKILL to those +/// that didn't die from SIGTERM after a while(exit_timeout is 0). +static void job_prepare_cb(uv_prepare_t *handle) +{ + Job *job; + int i; + + for (i = 0; i < MAX_RUNNING_JOBS; i++) { + if ((job = table[i]) == NULL || !job->stopped) { + continue; + } + + if ((job->exit_timeout--) == EXIT_TIMEOUT) { + // Job was just stopped, close all stdio handles and send SIGTERM + uv_process_kill(&job->proc, SIGTERM); + } else if (job->exit_timeout == 0) { + // We've waited long enough, send SIGKILL + uv_process_kill(&job->proc, SIGKILL); + } + } +} + +// Wraps the call to std{out,err}_cb and emits a JobExit event if necessary. +static void read_cb(RStream *rstream, void *data, bool eof) +{ + Job *job = data; + + if (rstream == job->out) { + job->stdout_cb(rstream, data, eof); + } else { + job->stderr_cb(rstream, data, eof); + } + + if (eof && --job->pending_refs == 0) { + emit_exit_event(job); + } +} + +// Emits a JobExit event if both rstreams are closed +static void exit_cb(uv_process_t *proc, int64_t status, int term_signal) +{ + Job *job = handle_get_job((uv_handle_t *)proc); + + if (--job->pending_refs == 0) { + emit_exit_event(job); + } +} + +static void emit_exit_event(Job *job) +{ + Event event; + event.type = kEventJobExit; + event.data.job = job; + event_push(event); +} + +static void close_cb(uv_handle_t *handle) +{ + Job *job = handle_get_job(handle); + + if (--job->pending_closes == 0) { + // Only free the job memory after all the associated handles are properly + // closed by libuv + rstream_free(job->out); + rstream_free(job->err); + wstream_free(job->in); + shell_free_argv(job->proc_opts.args); + free(job->data); + free(job); + } +} diff --git a/src/nvim/os/job.h b/src/nvim/os/job.h new file mode 100644 index 0000000000..db147f2c24 --- /dev/null +++ b/src/nvim/os/job.h @@ -0,0 +1,78 @@ +// Job is a short name we use to refer to child processes that run in parallel +// with the editor, probably executing long-running tasks and sending updates +// asynchronously. Communication happens through anonymous pipes connected to +// the job's std{in,out,err}. They are more like bash/zsh co-processes than the +// usual shell background job. The name 'Job' was chosen because it applies to +// the concept while being significantly shorter. +#ifndef NEOVIM_OS_JOB_H +#define NEOVIM_OS_JOB_H + +#include <stdint.h> +#include <stdbool.h> + +#include "os/event_defs.h" +#include "os/rstream_defs.h" + +/// Initializes job control resources +void job_init(void); + +/// Releases job control resources and terminates running jobs +void job_teardown(void); + +/// Tries to start a new job. +/// +/// @param argv Argument vector for the process. The first item is the +/// executable to run. +/// @param data Caller data that will be associated with the job +/// @param stdout_cb Callback that will be invoked when data is available +/// on stdout +/// @param stderr_cb Callback that will be invoked when data is available +/// on stderr +/// @param exit_cb Callback that will be invoked when the job exits. This is +/// optional. +/// @return The job id if the job started successfully. If the the first item / +/// of `argv`(the program) could not be executed, -1 will be returned. +// 0 will be returned if the job table is full. +int job_start(char **argv, + void *data, + rstream_cb stdout_cb, + rstream_cb stderr_cb, + job_exit_cb exit_cb); + +/// Terminates a job. This is a non-blocking operation, but if the job exists +/// it's guaranteed to succeed(SIGKILL will eventually be sent) +/// +/// @param id The job id +/// @return true if the stop request was successfully sent, false if the job +/// id is invalid(probably because it has already stopped) +bool job_stop(int id); + +/// Writes data to the job's stdin. This is a non-blocking operation, it +/// returns when the write request was sent. +/// +/// @param id The job id +/// @param data Buffer containing the data to be written +/// @param len Size of the data +/// @return true if the write request was successfully sent, false if the job +/// id is invalid(probably because it has already stopped) +bool job_write(int id, char *data, uint32_t len); + +/// Runs the read callback associated with the job exit event +/// +/// @param event Object containing data necessary to invoke the callback +void job_exit_event(Event event); + +/// Get the job id +/// +/// @param job A pointer to the job +/// @return The job id +int job_id(Job *job); + +/// Get data associated with a job +/// +/// @param job A pointer to the job +/// @return The job data +void *job_data(Job *job); + +#endif // NEOVIM_OS_JOB_H + diff --git a/src/nvim/os/job_defs.h b/src/nvim/os/job_defs.h new file mode 100644 index 0000000000..e98b639154 --- /dev/null +++ b/src/nvim/os/job_defs.h @@ -0,0 +1,15 @@ +#ifndef NEOVIM_OS_JOB_DEFS_H +#define NEOVIM_OS_JOB_DEFS_H + +#include "os/rstream_defs.h" + +typedef struct job Job; + +/// Function called when the job reads data +/// +/// @param id The job id +/// @param data Some data associated with the job by the caller +typedef void (*job_exit_cb)(Job *job, void *data); + +#endif // NEOVIM_OS_JOB_DEFS_H + diff --git a/src/nvim/os/mem.c b/src/nvim/os/mem.c new file mode 100644 index 0000000000..e5549340ff --- /dev/null +++ b/src/nvim/os/mem.c @@ -0,0 +1,10 @@ +/// Functions for accessing system memory information. + +#include <uv.h> + +#include "os/os.h" + +uint64_t os_get_total_mem_kib(void) { + // Convert bytes to KiB. + return uv_get_total_memory() >> 10; +} diff --git a/src/nvim/os/msgpack_rpc.c b/src/nvim/os/msgpack_rpc.c new file mode 100644 index 0000000000..e429bf0519 --- /dev/null +++ b/src/nvim/os/msgpack_rpc.c @@ -0,0 +1,408 @@ +#include <msgpack.h> + +#include "msgpack_rpc.h" +#include "vim.h" +#include "memory.h" + +static bool msgpack_rpc_to_uint16_t(msgpack_object *obj, uint16_t *arg); + +void msgpack_rpc_call(msgpack_object *req, msgpack_packer *res) +{ + // The initial response structure is the same no matter what happens, + // we set it up here + // Array of size 4 + msgpack_pack_array(res, 4); + // Response type is 1 + msgpack_pack_int(res, 1); + + // Validate the basic structure of the msgpack-rpc payload + if (req->type != MSGPACK_OBJECT_ARRAY) { + msgpack_pack_int(res, 0); // no message id yet + msgpack_rpc_error("Request is not an array", res); + return; + } + + if (req->via.array.size != 4) { + msgpack_pack_int(res, 0); // no message id yet + char error_msg[256]; + snprintf(error_msg, + sizeof(error_msg), + "Request array size is %u, it should be 4", + req->via.array.size); + msgpack_rpc_error(error_msg, res); + } + + if (req->via.array.ptr[1].type != MSGPACK_OBJECT_POSITIVE_INTEGER) { + msgpack_pack_int(res, 0); // no message id yet + msgpack_rpc_error("Id must be a positive integer", res); + } + + // Set the response id, which is the same as the request + msgpack_pack_int(res, req->via.array.ptr[1].via.u64); + + if (req->via.array.ptr[0].type != MSGPACK_OBJECT_POSITIVE_INTEGER) { + msgpack_rpc_error("Message type must be an integer", res); + return; + } + + if (req->via.array.ptr[0].via.u64 != 0) { + msgpack_rpc_error("Message type must be 0", res); + return; + } + + if (req->via.array.ptr[2].type != MSGPACK_OBJECT_POSITIVE_INTEGER) { + msgpack_rpc_error("Method id must be a positive integer", res); + return; + } + + if (req->via.array.ptr[3].type != MSGPACK_OBJECT_ARRAY) { + msgpack_rpc_error("Paremeters must be an array", res); + return; + } + + // dispatch the message + msgpack_rpc_dispatch(req, res); +} + +void msgpack_rpc_error(char *msg, msgpack_packer *res) +{ + size_t len = strlen(msg); + + // error message + msgpack_pack_raw(res, len); + msgpack_pack_raw_body(res, msg, len); + // Nil result + msgpack_pack_nil(res); +} + +bool msgpack_rpc_to_bool(msgpack_object *obj, bool *arg) +{ + *arg = obj->via.boolean; + return obj->type == MSGPACK_OBJECT_BOOLEAN; +} + +bool msgpack_rpc_to_int64_t(msgpack_object *obj, int64_t *arg) +{ + *arg = obj->via.i64; + return obj->type == MSGPACK_OBJECT_POSITIVE_INTEGER + || obj->type == MSGPACK_OBJECT_NEGATIVE_INTEGER; +} + +bool msgpack_rpc_to_double(msgpack_object *obj, double *arg) +{ + *arg = obj->via.dec; + return obj->type == MSGPACK_OBJECT_DOUBLE; +} + +bool msgpack_rpc_to_string(msgpack_object *obj, String *arg) +{ + arg->data = (char *)obj->via.raw.ptr; + arg->size = obj->via.raw.size; + return obj->type == MSGPACK_OBJECT_RAW; +} + +bool msgpack_rpc_to_buffer(msgpack_object *obj, Buffer *arg) +{ + return msgpack_rpc_to_uint16_t(obj, arg); +} + +bool msgpack_rpc_to_window(msgpack_object *obj, Window *arg) +{ + return msgpack_rpc_to_uint16_t(obj, arg); +} + +bool msgpack_rpc_to_tabpage(msgpack_object *obj, Tabpage *arg) +{ + return msgpack_rpc_to_uint16_t(obj, arg); +} + +bool msgpack_rpc_to_object(msgpack_object *obj, Object *arg) +{ + switch (obj->type) { + case MSGPACK_OBJECT_NIL: + arg->type = kObjectTypeNil; + return true; + + case MSGPACK_OBJECT_BOOLEAN: + arg->type = kObjectTypeBool; + return msgpack_rpc_to_bool(obj, &arg->data.boolean); + + case MSGPACK_OBJECT_POSITIVE_INTEGER: + case MSGPACK_OBJECT_NEGATIVE_INTEGER: + arg->type = kObjectTypeInt; + return msgpack_rpc_to_int64_t(obj, &arg->data.integer); + + case MSGPACK_OBJECT_DOUBLE: + arg->type = kObjectTypeFloat; + return msgpack_rpc_to_double(obj, &arg->data.floating_point); + + case MSGPACK_OBJECT_RAW: + arg->type = kObjectTypeString; + return msgpack_rpc_to_string(obj, &arg->data.string); + + case MSGPACK_OBJECT_ARRAY: + arg->type = kObjectTypeArray; + return msgpack_rpc_to_array(obj, &arg->data.array); + + case MSGPACK_OBJECT_MAP: + arg->type = kObjectTypeDictionary; + return msgpack_rpc_to_dictionary(obj, &arg->data.dictionary); + + default: + return false; + } +} + +bool msgpack_rpc_to_stringarray(msgpack_object *obj, StringArray *arg) +{ + if (obj->type != MSGPACK_OBJECT_ARRAY) { + return false; + } + + arg->size = obj->via.array.size; + arg->items = xcalloc(obj->via.array.size, sizeof(String)); + + for (uint32_t i = 0; i < obj->via.array.size; i++) { + if (!msgpack_rpc_to_string(obj->via.array.ptr + i, &arg->items[i])) { + return false; + } + } + + return true; +} + +bool msgpack_rpc_to_position(msgpack_object *obj, Position *arg) +{ + // positions are represented by integer arrays of size 2 + if (obj->type != MSGPACK_OBJECT_ARRAY + || obj->via.array.size != 2 + || obj->via.array.ptr[0].type != MSGPACK_OBJECT_POSITIVE_INTEGER + || obj->via.array.ptr[1].type != MSGPACK_OBJECT_POSITIVE_INTEGER) { + return false; + } + + arg->row = obj->via.array.ptr[0].via.u64; + arg->col = obj->via.array.ptr[1].via.u64; + + return true; +} + + +bool msgpack_rpc_to_array(msgpack_object *obj, Array *arg) +{ + if (obj->type != MSGPACK_OBJECT_ARRAY) { + return false; + } + + arg->size = obj->via.array.size; + arg->items = xcalloc(obj->via.array.size, sizeof(Object)); + + for (uint32_t i = 0; i < obj->via.array.size; i++) { + if (!msgpack_rpc_to_object(obj->via.array.ptr + i, &arg->items[i])) { + return false; + } + } + + return true; +} + +bool msgpack_rpc_to_dictionary(msgpack_object *obj, Dictionary *arg) +{ + if (obj->type != MSGPACK_OBJECT_MAP) { + return false; + } + + arg->size = obj->via.array.size; + arg->items = xcalloc(obj->via.map.size, sizeof(KeyValuePair)); + + + for (uint32_t i = 0; i < obj->via.map.size; i++) { + if (!msgpack_rpc_to_string(&obj->via.map.ptr[i].key, + &arg->items[i].key)) { + return false; + } + + if (!msgpack_rpc_to_object(&obj->via.map.ptr[i].val, + &arg->items[i].value)) { + return false; + } + } + + return true; +} + +void msgpack_rpc_from_bool(bool result, msgpack_packer *res) +{ + if (result) { + msgpack_pack_true(res); + } else { + msgpack_pack_false(res); + } +} + +void msgpack_rpc_from_int64_t(int64_t result, msgpack_packer *res) +{ + msgpack_pack_int64(res, result); +} + +void msgpack_rpc_from_uint64_t(uint64_t result, msgpack_packer *res) +{ + msgpack_pack_uint64(res, result); +} + +void msgpack_rpc_from_double(double result, msgpack_packer *res) +{ + msgpack_pack_double(res, result); +} + +void msgpack_rpc_from_string(String result, msgpack_packer *res) +{ + msgpack_pack_raw(res, result.size); + msgpack_pack_raw_body(res, result.data, result.size); +} + +void msgpack_rpc_from_buffer(Buffer result, msgpack_packer *res) +{ + msgpack_rpc_from_uint64_t(result, res); +} + +void msgpack_rpc_from_window(Window result, msgpack_packer *res) +{ + msgpack_rpc_from_uint64_t(result, res); +} + +void msgpack_rpc_from_tabpage(Tabpage result, msgpack_packer *res) +{ + msgpack_rpc_from_uint64_t(result, res); +} + +void msgpack_rpc_from_object(Object result, msgpack_packer *res) +{ + switch (result.type) { + case kObjectTypeNil: + msgpack_pack_nil(res); + break; + + case kObjectTypeBool: + msgpack_rpc_from_bool(result.data.boolean, res); + break; + + case kObjectTypeInt: + msgpack_rpc_from_int64_t(result.data.integer, res); + break; + + case kObjectTypeFloat: + msgpack_rpc_from_double(result.data.floating_point, res); + break; + + case kObjectTypeString: + msgpack_rpc_from_string(result.data.string, res); + break; + + case kObjectTypeArray: + msgpack_rpc_from_array(result.data.array, res); + break; + + case kObjectTypeDictionary: + msgpack_rpc_from_dictionary(result.data.dictionary, res); + break; + + default: + abort(); + } +} + +void msgpack_rpc_from_stringarray(StringArray result, msgpack_packer *res) +{ + msgpack_pack_array(res, result.size); + + for (size_t i = 0; i < result.size; i++) { + msgpack_rpc_from_string(result.items[i], res); + } +} + +void msgpack_rpc_from_position(Position result, msgpack_packer *res) +{ + msgpack_pack_array(res, 2);; + msgpack_pack_uint16(res, result.row); + msgpack_pack_uint16(res, result.col); +} + +void msgpack_rpc_from_array(Array result, msgpack_packer *res) +{ + msgpack_pack_array(res, result.size); + + for (size_t i = 0; i < result.size; i++) { + msgpack_rpc_from_object(result.items[i], res); + } +} + +void msgpack_rpc_from_dictionary(Dictionary result, msgpack_packer *res) +{ + msgpack_pack_map(res, result.size); + + for (size_t i = 0; i < result.size; i++) { + msgpack_rpc_from_string(result.items[i].key, res); + msgpack_rpc_from_object(result.items[i].value, res); + } +} + +void msgpack_rpc_free_object(Object value) +{ + switch (value.type) { + case kObjectTypeNil: + case kObjectTypeBool: + case kObjectTypeInt: + case kObjectTypeFloat: + break; + + case kObjectTypeString: + msgpack_rpc_free_string(value.data.string); + break; + + case kObjectTypeArray: + msgpack_rpc_free_array(value.data.array); + break; + + case kObjectTypeDictionary: + msgpack_rpc_free_dictionary(value.data.dictionary); + break; + + default: + abort(); + } +} + +void msgpack_rpc_free_stringarray(StringArray value) { + for (uint32_t i = 0; i < value.size; i++) { + msgpack_rpc_free_string(value.items[i]); + } + + free(value.items); +} + +void msgpack_rpc_free_array(Array value) +{ + for (uint32_t i = 0; i < value.size; i++) { + msgpack_rpc_free_object(value.items[i]); + } + + free(value.items); +} + +void msgpack_rpc_free_dictionary(Dictionary value) +{ + for (uint32_t i = 0; i < value.size; i++) { + msgpack_rpc_free_string(value.items[i].key); + msgpack_rpc_free_object(value.items[i].value); + } + + free(value.items); +} + +static bool msgpack_rpc_to_uint16_t(msgpack_object *obj, uint16_t *arg) +{ + *arg = obj->via.u64; + return obj->type == MSGPACK_OBJECT_POSITIVE_INTEGER; +} + diff --git a/src/nvim/os/msgpack_rpc.h b/src/nvim/os/msgpack_rpc.h new file mode 100644 index 0000000000..7f754bfca1 --- /dev/null +++ b/src/nvim/os/msgpack_rpc.h @@ -0,0 +1,107 @@ +#ifndef NEOVIM_OS_MSGPACK_RPC_H +#define NEOVIM_OS_MSGPACK_RPC_H + +#include <stdint.h> +#include <stdbool.h> + +#include <msgpack.h> + +#include "api/defs.h" + +/// Validates the basic structure of the msgpack-rpc call and fills `res` +/// with the basic response structure. +/// +/// @param req The parsed request object +/// @param res A packer that contains the response +void msgpack_rpc_call(msgpack_object *req, msgpack_packer *res); + +/// Dispatches to the actual API function after basic payload validation by +/// `msgpack_rpc_call`. It is responsible for validating/converting arguments +/// to C types, and converting the return value back to msgpack types. +/// The implementation is generated at compile time with metadata extracted +/// from the api/*.h headers, +/// +/// @param req The parsed request object +/// @param res A packer that contains the response +void msgpack_rpc_dispatch(msgpack_object *req, msgpack_packer *res); + +/// Finishes the msgpack-rpc call with an error message. +/// +/// @param msg The error message +/// @param res A packer that contains the response +void msgpack_rpc_error(char *msg, msgpack_packer *res); + +/// Functions for validating and converting from msgpack types to C types. +/// These are used by `msgpack_rpc_dispatch` to validate and convert each +/// argument. +/// +/// @param obj The object to convert +/// @param[out] arg A pointer to the avalue +/// @return true if the convertion succeeded, false otherwise +bool msgpack_rpc_to_bool(msgpack_object *obj, bool *arg); +bool msgpack_rpc_to_int64_t(msgpack_object *obj, int64_t *arg); +bool msgpack_rpc_to_double(msgpack_object *obj, double *arg); +bool msgpack_rpc_to_position(msgpack_object *obj, Position *arg); +bool msgpack_rpc_to_string(msgpack_object *obj, String *arg); +bool msgpack_rpc_to_buffer(msgpack_object *obj, Buffer *arg); +bool msgpack_rpc_to_window(msgpack_object *obj, Window *arg); +bool msgpack_rpc_to_tabpage(msgpack_object *obj, Tabpage *arg); +bool msgpack_rpc_to_object(msgpack_object *obj, Object *arg); +bool msgpack_rpc_to_stringarray(msgpack_object *obj, StringArray *arg); +bool msgpack_rpc_to_array(msgpack_object *obj, Array *arg); +bool msgpack_rpc_to_dictionary(msgpack_object *obj, Dictionary *arg); + +/// Functions for converting from C types to msgpack types. +/// These are used by `msgpack_rpc_dispatch` to convert return values +/// from the API +/// +/// @param result A pointer to the result +/// @param res A packer that contains the response +void msgpack_rpc_from_bool(bool result, msgpack_packer *res); +void msgpack_rpc_from_int64_t(int64_t result, msgpack_packer *res); +void msgpack_rpc_from_double(double result, msgpack_packer *res); +void msgpack_rpc_from_position(Position result, msgpack_packer *res); +void msgpack_rpc_from_string(String result, msgpack_packer *res); +void msgpack_rpc_from_buffer(Buffer result, msgpack_packer *res); +void msgpack_rpc_from_window(Window result, msgpack_packer *res); +void msgpack_rpc_from_tabpage(Tabpage result, msgpack_packer *res); +void msgpack_rpc_from_object(Object result, msgpack_packer *res); +void msgpack_rpc_from_stringarray(StringArray result, msgpack_packer *res); +void msgpack_rpc_from_array(Array result, msgpack_packer *res); +void msgpack_rpc_from_dictionary(Dictionary result, msgpack_packer *res); + +/// Helpers for initializing types that may be freed later +#define msgpack_rpc_init_bool +#define msgpack_rpc_init_int64_t +#define msgpack_rpc_init_double +#define msgpack_rpc_init_position +#define msgpack_rpc_init_string +#define msgpack_rpc_init_buffer +#define msgpack_rpc_init_window +#define msgpack_rpc_init_tabpage +#define msgpack_rpc_init_object = {.type = kObjectTypeNil} +#define msgpack_rpc_init_stringarray = {.items = NULL, .size = 0} +#define msgpack_rpc_init_array = {.items = NULL, .size = 0} +#define msgpack_rpc_init_dictionary = {.items = NULL, .size = 0} + +/// Helpers for freeing arguments/return value +/// +/// @param value The value to be freed +#define msgpack_rpc_free_bool(value) +#define msgpack_rpc_free_int64_t(value) +#define msgpack_rpc_free_double(value) +#define msgpack_rpc_free_position(value) +// Strings are not copied from msgpack and so don't need to be freed(they +// probably "live" in the msgpack streaming buffer) +#define msgpack_rpc_free_string(value) +#define msgpack_rpc_free_buffer(value) +#define msgpack_rpc_free_window(value) +#define msgpack_rpc_free_tabpage(value) +void msgpack_rpc_free_object(Object value); +void msgpack_rpc_free_stringarray(StringArray value); +void msgpack_rpc_free_array(Array value); +void msgpack_rpc_free_dictionary(Dictionary value); + + +#endif // NEOVIM_OS_MSGPACK_RPC_H + diff --git a/src/nvim/os/os.h b/src/nvim/os/os.h new file mode 100644 index 0000000000..b30872f06d --- /dev/null +++ b/src/nvim/os/os.h @@ -0,0 +1,146 @@ +#ifndef NEOVIM_OS_OS_H +#define NEOVIM_OS_OS_H +#include <uv.h> + +#include "vim.h" + +/// Change to the given directory. +/// +/// @return `0` on success, a libuv error code on failure. +int os_chdir(const char *path); + +/// Get the name of current directory. +/// +/// @param buf Buffer to store the directory name. +/// @param len Length of `buf`. +/// @return `OK` for success, `FAIL` for failure. +int os_dirname(char_u *buf, size_t len); + +/// Check if the given path is a directory or not. +/// +/// @return `true` if `fname` is a directory. +bool os_isdir(const char_u *name); + +/// Check if the given path represents an executable file. +/// +/// @return `true` if `name` is executable and +/// - can be found in $PATH, +/// - is relative to current dir or +/// - is absolute. +/// +/// @return `false` otherwise. +bool os_can_exe(const char_u *name); + +/// Get the file permissions for a given file. +/// +/// @return `-1` when `name` doesn't exist. +int32_t os_getperm(const char_u *name); + +/// Set the permission of a file. +/// +/// @return `OK` for success, `FAIL` for failure. +int os_setperm(const char_u *name, int perm); + +/// Check if a file exists. +/// +/// @return `true` if `name` exists. +bool os_file_exists(const char_u *name); + +/// Check if a file is readonly. +/// +/// @return `true` if `name` is readonly. +bool os_file_is_readonly(const char *name); + +/// Check if a file is writable. +/// +/// @return `0` if `name` is not writable, +/// @return `1` if `name` is writable, +/// @return `2` for a directory which we have rights to write into. +int os_file_is_writable(const char *name); + +/// Get the size of a file in bytes. +/// +/// @param[out] size pointer to an off_t to put the size into. +/// @return `true` for success, `false` for failure. +bool os_get_file_size(const char *name, off_t *size); + +/// Rename a file or directory. +/// +/// @return `OK` for success, `FAIL` for failure. +int os_rename(const char_u *path, const char_u *new_path); + +/// Make a directory. +/// +/// @return `0` for success, non-zero for failure. +int os_mkdir(const char *path, int32_t mode); + +/// Remove a directory. +/// +/// @return `0` for success, non-zero for failure. +int os_rmdir(const char *path); + +/// Remove a file. +/// +/// @return `0` for success, non-zero for failure. +int os_remove(const char *path); + +/// Get the total system physical memory in KiB. +uint64_t os_get_total_mem_kib(void); +const char *os_getenv(const char *name); +int os_setenv(const char *name, const char *value, int overwrite); +char *os_getenvname_at_index(size_t index); + +/// Get the process ID of the Neovim process. +/// +/// @return the process ID. +int64_t os_get_pid(void); + +/// Get the hostname of the machine runing Neovim. +/// +/// @param hostname Buffer to store the hostname. +/// @param len Length of `hostname`. +void os_get_hostname(char *hostname, size_t len); + +int os_get_usernames(garray_T *usernames); +int os_get_user_name(char *s, size_t len); +int os_get_uname(uid_t uid, char *s, size_t len); +char *os_get_user_directory(const char *name); + +/// Get stat information for a file. +/// +/// @return OK on success, FAIL if an failure occured. +int os_stat(const char_u *name, uv_stat_t *statbuf); + +/// Struct which encapsulates stat information. +typedef struct { + // TODO(stefan991): make stat private + uv_stat_t stat; +} FileInfo; + +/// Get the file information for a given path +/// +/// @param file_descriptor File descriptor of the file. +/// @param[out] file_info Pointer to a FileInfo to put the information in. +/// @return `true` on sucess, `false` for failure. +bool os_get_file_info(const char *path, FileInfo *file_info); + +/// Get the file information for a given path without following links +/// +/// @param path Path to the file. +/// @param[out] file_info Pointer to a FileInfo to put the information in. +/// @return `true` on sucess, `false` for failure. +bool os_get_file_info_link(const char *path, FileInfo *file_info); + +/// Get the file information for a given file descriptor +/// +/// @param file_descriptor File descriptor of the file. +/// @param[out] file_info Pointer to a FileInfo to put the information in. +/// @return `true` on sucess, `false` for failure. +bool os_get_file_info_fd(int file_descriptor, FileInfo *file_info); + +/// Compare the inodes of two FileInfos +/// +/// @return `true` if the two FileInfos represent the same file. +bool os_file_info_id_equal(FileInfo *file_info_1, FileInfo *file_info_2); + +#endif // NEOVIM_OS_OS_H diff --git a/src/nvim/os/rstream.c b/src/nvim/os/rstream.c new file mode 100644 index 0000000000..e14075c8db --- /dev/null +++ b/src/nvim/os/rstream.c @@ -0,0 +1,297 @@ +#include <stdint.h> +#include <stdbool.h> +#include <stdlib.h> + +#include <uv.h> + +#include "os/uv_helpers.h" +#include "os/rstream_defs.h" +#include "os/rstream.h" +#include "os/event_defs.h" +#include "os/event.h" +#include "vim.h" +#include "memory.h" + +struct rstream { + uv_buf_t uvbuf; + void *data; + char *buffer; + uv_stream_t *stream; + uv_idle_t *fread_idle; + uv_handle_type file_type; + uv_file fd; + rstream_cb cb; + size_t buffer_size, rpos, wpos, fpos; + bool reading, free_handle, async; +}; + +// Callbacks used by libuv +static void alloc_cb(uv_handle_t *, size_t, uv_buf_t *); +static void read_cb(uv_stream_t *, ssize_t, const uv_buf_t *); +static void fread_idle_cb(uv_idle_t *); +static void close_cb(uv_handle_t *handle); +static void emit_read_event(RStream *rstream, bool eof); + +RStream * rstream_new(rstream_cb cb, + uint32_t buffer_size, + void *data, + bool async) +{ + RStream *rv = xmalloc(sizeof(RStream)); + rv->buffer = xmalloc(buffer_size); + rv->buffer_size = buffer_size; + rv->data = data; + rv->async = async; + rv->cb = cb; + rv->rpos = rv->wpos = rv->fpos = 0; + rv->stream = NULL; + rv->fread_idle = NULL; + rv->free_handle = false; + rv->file_type = UV_UNKNOWN_HANDLE; + + return rv; +} + +void rstream_free(RStream *rstream) +{ + if (rstream->free_handle) { + if (rstream->fread_idle != NULL) { + uv_close((uv_handle_t *)rstream->fread_idle, close_cb); + } else { + uv_close((uv_handle_t *)rstream->stream, close_cb); + } + } + + free(rstream->buffer); + free(rstream); +} + +void rstream_set_stream(RStream *rstream, uv_stream_t *stream) +{ + handle_set_rstream((uv_handle_t *)stream, rstream); + rstream->stream = stream; +} + +void rstream_set_file(RStream *rstream, uv_file file) +{ + rstream->file_type = uv_guess_handle(file); + + if (rstream->free_handle) { + // If this is the second time we're calling this function, free the + // previously allocated memory + if (rstream->fread_idle != NULL) { + uv_close((uv_handle_t *)rstream->fread_idle, close_cb); + } else { + uv_close((uv_handle_t *)rstream->stream, close_cb); + } + } + + if (rstream->file_type == UV_FILE) { + // Non-blocking file reads are simulated with a idle handle that reads + // in chunks of rstream->buffer_size, giving time for other events to + // be processed between reads. + rstream->fread_idle = xmalloc(sizeof(uv_idle_t)); + uv_idle_init(uv_default_loop(), rstream->fread_idle); + rstream->fread_idle->data = NULL; + handle_set_rstream((uv_handle_t *)rstream->fread_idle, rstream); + } else { + // Only pipes are supported for now + assert(rstream->file_type == UV_NAMED_PIPE + || rstream->file_type == UV_TTY); + rstream->stream = xmalloc(sizeof(uv_pipe_t)); + uv_pipe_init(uv_default_loop(), (uv_pipe_t *)rstream->stream, 0); + uv_pipe_open((uv_pipe_t *)rstream->stream, file); + rstream->stream->data = NULL; + handle_set_rstream((uv_handle_t *)rstream->stream, rstream); + } + + rstream->fd = file; + rstream->free_handle = true; +} + +bool rstream_is_regular_file(RStream *rstream) +{ + return rstream->file_type == UV_FILE; +} + +void rstream_start(RStream *rstream) +{ + if (rstream->file_type == UV_FILE) { + uv_idle_start(rstream->fread_idle, fread_idle_cb); + } else { + rstream->reading = false; + uv_read_start(rstream->stream, alloc_cb, read_cb); + } +} + +void rstream_stop(RStream *rstream) +{ + if (rstream->file_type == UV_FILE) { + uv_idle_stop(rstream->fread_idle); + } else { + uv_read_stop(rstream->stream); + } +} + +size_t rstream_read(RStream *rstream, char *buf, uint32_t count) +{ + size_t read_count = rstream->wpos - rstream->rpos; + + if (count < read_count) { + read_count = count; + } + + if (read_count > 0) { + memcpy(buf, rstream->buffer + rstream->rpos, read_count); + rstream->rpos += read_count; + } + + if (rstream->wpos == rstream->buffer_size) { + // `wpos` is at the end of the buffer, so free some space by moving unread + // data... + memmove( + rstream->buffer, // ...To the beginning of the buffer(rpos 0) + rstream->buffer + rstream->rpos, // ...From the first unread position + rstream->wpos - rstream->rpos); // ...By the number of unread bytes + rstream->wpos -= rstream->rpos; + rstream->rpos = 0; + + if (rstream->wpos < rstream->buffer_size) { + // Restart reading since we have freed some space + rstream_start(rstream); + } + } + + return read_count; +} + +size_t rstream_available(RStream *rstream) +{ + return rstream->wpos - rstream->rpos; +} + +void rstream_read_event(Event event) +{ + RStream *rstream = event.data.rstream.ptr; + + rstream->cb(rstream, rstream->data, event.data.rstream.eof); +} + +// Called by libuv to allocate memory for reading. +static void alloc_cb(uv_handle_t *handle, size_t suggested, uv_buf_t *buf) +{ + RStream *rstream = handle_get_rstream(handle); + + if (rstream->reading) { + buf->len = 0; + return; + } + + buf->len = rstream->buffer_size - rstream->wpos; + buf->base = rstream->buffer + rstream->wpos; + + // Avoid `alloc_cb`, `alloc_cb` sequences on windows + rstream->reading = true; +} + +// Callback invoked by libuv after it copies the data into the buffer provided +// by `alloc_cb`. This is also called on EOF or when `alloc_cb` returns a +// 0-length buffer. +static void read_cb(uv_stream_t *stream, ssize_t cnt, const uv_buf_t *buf) +{ + RStream *rstream = handle_get_rstream((uv_handle_t *)stream); + + if (cnt <= 0) { + if (cnt != UV_ENOBUFS) { + // Read error or EOF, either way stop the stream and invoke the callback + // with eof == true + uv_read_stop(stream); + emit_read_event(rstream, true); + } + return; + } + + // at this point we're sure that cnt is positive, no error occurred + size_t nread = (size_t) cnt; + + // Data was already written, so all we need is to update 'wpos' to reflect + // the space actually used in the buffer. + rstream->wpos += nread; + + if (rstream->wpos == rstream->buffer_size) { + // The last read filled the buffer, stop reading for now + rstream_stop(rstream); + } + + rstream->reading = false; + emit_read_event(rstream, false); +} + +// Called by the by the 'idle' handle to emulate a reading event +static void fread_idle_cb(uv_idle_t *handle) +{ + uv_fs_t req; + RStream *rstream = handle_get_rstream((uv_handle_t *)handle); + + rstream->uvbuf.base = rstream->buffer + rstream->wpos; + rstream->uvbuf.len = rstream->buffer_size - rstream->wpos; + + // the offset argument to uv_fs_read is int64_t, could someone really try + // to read more than 9 quintillion (9e18) bytes? + // upcast is meant to avoid tautological condition warning on 32 bits + uintmax_t fpos_intmax = rstream->fpos; + assert(fpos_intmax <= INT64_MAX); + + // Synchronous read + uv_fs_read( + uv_default_loop(), + &req, + rstream->fd, + &rstream->uvbuf, + 1, + (int64_t) rstream->fpos, + NULL); + + uv_fs_req_cleanup(&req); + + if (req.result <= 0) { + uv_idle_stop(rstream->fread_idle); + emit_read_event(rstream, true); + return; + } + + // no errors (req.result (ssize_t) is positive), it's safe to cast. + size_t nread = (size_t) req.result; + + rstream->wpos += nread; + rstream->fpos += nread; + + if (rstream->wpos == rstream->buffer_size) { + // The last read filled the buffer, stop reading for now + rstream_stop(rstream); + } + + emit_read_event(rstream, false); +} + +static void close_cb(uv_handle_t *handle) +{ + free(handle->data); + free(handle); +} + +static void emit_read_event(RStream *rstream, bool eof) +{ + if (rstream->async) { + Event event; + + event.type = kEventRStreamData; + event.data.rstream.ptr = rstream; + event.data.rstream.eof = eof; + event_push(event); + } else { + // Invoke the callback passing in the number of bytes available and data + // associated with the stream + rstream->cb(rstream, rstream->data, eof); + } +} diff --git a/src/nvim/os/rstream.h b/src/nvim/os/rstream.h new file mode 100644 index 0000000000..5eb3e97f55 --- /dev/null +++ b/src/nvim/os/rstream.h @@ -0,0 +1,82 @@ +#ifndef NEOVIM_OS_RSTREAM_H +#define NEOVIM_OS_RSTREAM_H + +#include <stdbool.h> +#include <stdint.h> +#include <uv.h> + +#include "os/event_defs.h" +#include "os/rstream_defs.h" + +/// Creates a new RStream instance. A RStream encapsulates all the boilerplate +/// necessary for reading from a libuv stream. +/// +/// @param cb A function that will be called whenever some data is available +/// for reading with `rstream_read` +/// @param buffer_size Size in bytes of the internal buffer. +/// @param data Some state to associate with the `RStream` instance +/// @param async Flag that specifies if the callback should only be called +/// outside libuv event loop(When processing async events with +/// KE_EVENT). Only the RStream instance reading user input should set +/// this to false +/// @return The newly-allocated `RStream` instance +RStream * rstream_new(rstream_cb cb, + uint32_t buffer_size, + void *data, + bool async); + +/// Frees all memory allocated for a RStream instance +/// +/// @param rstream The `RStream` instance +void rstream_free(RStream *rstream); + +/// Sets the underlying `uv_stream_t` instance +/// +/// @param rstream The `RStream` instance +/// @param stream The new `uv_stream_t` instance +void rstream_set_stream(RStream *rstream, uv_stream_t *stream); + +/// Sets the underlying file descriptor that will be read from. Only pipes +/// and regular files are supported for now. +/// +/// @param rstream The `RStream` instance +/// @param file The file descriptor +void rstream_set_file(RStream *rstream, uv_file file); + +/// Tests if the stream is backed by a regular file +/// +/// @param rstream The `RStream` instance +/// @return True if the underlying file descriptor represents a regular file +bool rstream_is_regular_file(RStream *rstream); + +/// Starts watching for events from a `RStream` instance. +/// +/// @param rstream The `RStream` instance +void rstream_start(RStream *rstream); + +/// Stops watching for events from a `RStream` instance. +/// +/// @param rstream The `RStream` instance +void rstream_stop(RStream *rstream); + +/// Reads data from a `RStream` instance into a buffer. +/// +/// @param rstream The `RStream` instance +/// @param buffer The buffer which will receive the data +/// @param count Number of bytes that `buffer` can accept +/// @return The number of bytes copied into `buffer` +size_t rstream_read(RStream *rstream, char *buffer, uint32_t count); + +/// Returns the number of bytes available for reading from `rstream` +/// +/// @param rstream The `RStream` instance +/// @return The number of bytes available +size_t rstream_available(RStream *rstream); + +/// Runs the read callback associated with the rstream +/// +/// @param event Object containing data necessary to invoke the callback +void rstream_read_event(Event event); + +#endif // NEOVIM_OS_RSTREAM_H + diff --git a/src/nvim/os/rstream_defs.h b/src/nvim/os/rstream_defs.h new file mode 100644 index 0000000000..3d1dbec34f --- /dev/null +++ b/src/nvim/os/rstream_defs.h @@ -0,0 +1,16 @@ +#ifndef NEOVIM_OS_RSTREAM_DEFS_H +#define NEOVIM_OS_RSTREAM_DEFS_H + +#include <stdbool.h> + +typedef struct rstream RStream; + +/// Type of function called when the RStream receives data +/// +/// @param rstream The RStream instance +/// @param data State associated with the RStream instance +/// @param eof If the stream reached EOF. +typedef void (*rstream_cb)(RStream *rstream, void *data, bool eof); + +#endif // NEOVIM_OS_RSTREAM_DEFS_H + diff --git a/src/nvim/os/server.c b/src/nvim/os/server.c new file mode 100644 index 0000000000..bda9602936 --- /dev/null +++ b/src/nvim/os/server.c @@ -0,0 +1,243 @@ +#include <stdlib.h> +#include <string.h> +#include <stdint.h> +#include <stdbool.h> + +#include <uv.h> + +#include "os/channel_defs.h" +#include "os/channel.h" +#include "os/server.h" +#include "os/os.h" +#include "vim.h" +#include "memory.h" +#include "message.h" +#include "fileio.h" +#include "map.h" + +#define MAX_CONNECTIONS 32 +#define ADDRESS_MAX_SIZE 256 +#define NEOVIM_DEFAULT_TCP_PORT 7450 + +typedef enum { + kServerTypeTcp, + kServerTypePipe +} ServerType; + +typedef struct { + // Protocol for channels established through this server + ChannelProtocol protocol; + // Type of the union below + ServerType type; + + // This is either a tcp server or unix socket(named pipe on windows) + union { + struct { + uv_tcp_t handle; + struct sockaddr_in addr; + } tcp; + struct { + uv_pipe_t handle; + char addr[ADDRESS_MAX_SIZE]; + } pipe; + } socket; +} Server; + +static Map *servers = NULL; + +static void close_server(Map *map, const char *endpoint, void *server); +static void connection_cb(uv_stream_t *server, int status); +static void free_client(uv_handle_t *handle); +static void free_server(uv_handle_t *handle); + +void server_init() +{ + servers = map_new(); + + if (!os_getenv("NEOVIM_LISTEN_ADDRESS")) { + char *listen_address = (char *)vim_tempname('s'); + os_setenv("NEOVIM_LISTEN_ADDRESS", listen_address, 1); + free(listen_address); + } + + server_start((char *)os_getenv("NEOVIM_LISTEN_ADDRESS"), + kChannelProtocolMsgpack); +} + +void server_teardown() +{ + if (!servers) { + return; + } + + map_foreach(servers, close_server); +} + +void server_start(char *endpoint, ChannelProtocol prot) +{ + char addr[ADDRESS_MAX_SIZE]; + + // Trim to `ADDRESS_MAX_SIZE` + strncpy(addr, endpoint, sizeof(addr)); + + // Check if the server already exists + if (map_has(servers, addr)) { + EMSG2("Already listening on %s", addr); + return; + } + + ServerType server_type = kServerTypeTcp; + Server *server = xmalloc(sizeof(Server)); + char ip[16], *ip_end = strrchr(addr, ':'); + + server->protocol = prot; + + if (!ip_end) { + ip_end = strchr(addr, NUL); + } + + uint32_t addr_len = ip_end - addr; + + if (addr_len > sizeof(ip) - 1) { + // Maximum length of a ip address buffer is 15(eg: 255.255.255.255) + addr_len = sizeof(ip); + } + + // Extract the address part + strncpy(ip, addr, addr_len); + + int port = NEOVIM_DEFAULT_TCP_PORT; + + if (*ip_end == ':') { + char *port_end; + // Extract the port + port = strtol(ip_end + 1, &port_end, 10); + + errno = 0; + if (errno != 0 || port == 0 || port > 0xffff) { + // Invalid port, treat as named pipe or unix socket + server_type = kServerTypePipe; + } + } + + if (server_type == kServerTypeTcp) { + // Try to parse ip address + if (uv_ip4_addr(ip, port, &server->socket.tcp.addr)) { + // Invalid address, treat as named pipe or unix socket + server_type = kServerTypePipe; + } + } + + int result; + + if (server_type == kServerTypeTcp) { + // Listen on tcp address/port + uv_tcp_init(uv_default_loop(), &server->socket.tcp.handle); + server->socket.tcp.handle.data = server; + uv_tcp_bind(&server->socket.tcp.handle, + (const struct sockaddr *)&server->socket.tcp.addr, + 0); + result = uv_listen((uv_stream_t *)&server->socket.tcp.handle, + MAX_CONNECTIONS, + connection_cb); + if (result) { + uv_close((uv_handle_t *)&server->socket.tcp.handle, free_server); + } + } else { + // Listen on named pipe or unix socket + strcpy(server->socket.pipe.addr, addr); + uv_pipe_init(uv_default_loop(), &server->socket.pipe.handle, 0); + server->socket.pipe.handle.data = server; + uv_pipe_bind(&server->socket.pipe.handle, server->socket.pipe.addr); + result = uv_listen((uv_stream_t *)&server->socket.pipe.handle, + MAX_CONNECTIONS, + connection_cb); + + if (result) { + uv_close((uv_handle_t *)&server->socket.pipe.handle, free_server); + } + } + + if (result) { + EMSG2("Failed to start server: %s", uv_strerror(result)); + return; + } + + + server->type = server_type; + + // Add the server to the hash table + map_put(servers, addr, server); +} + +void server_stop(char *endpoint) +{ + Server *server; + char addr[ADDRESS_MAX_SIZE]; + + // Trim to `ADDRESS_MAX_SIZE` + strncpy(addr, endpoint, sizeof(addr)); + + if ((server = map_get(servers, addr)) == NULL) { + EMSG2("Not listening on %s", addr); + return; + } + + if (server->type == kServerTypeTcp) { + uv_close((uv_handle_t *)&server->socket.tcp.handle, free_server); + } else { + uv_close((uv_handle_t *)&server->socket.pipe.handle, free_server); + } + + map_del(servers, addr); +} + +static void connection_cb(uv_stream_t *server, int status) +{ + int result; + uv_stream_t *client; + Server *srv = server->data; + + if (status < 0) { + abort(); + } + + if (srv->type == kServerTypeTcp) { + client = xmalloc(sizeof(uv_tcp_t)); + uv_tcp_init(uv_default_loop(), (uv_tcp_t *)client); + } else { + client = xmalloc(sizeof(uv_pipe_t)); + uv_pipe_init(uv_default_loop(), (uv_pipe_t *)client, 0); + } + + result = uv_accept(server, client); + + if (result) { + EMSG2("Failed to accept connection: %s", uv_strerror(result)); + uv_close((uv_handle_t *)client, free_client); + return; + } + + channel_from_stream(client, srv->protocol); +} + +static void close_server(Map *map, const char *endpoint, void *srv) +{ + Server *server = srv; + + if (server->type == kServerTypeTcp) { + uv_close((uv_handle_t *)&server->socket.tcp.handle, free_server); + } else { + uv_close((uv_handle_t *)&server->socket.pipe.handle, free_server); + } +} + +static void free_client(uv_handle_t *handle) +{ + free(handle); +} + +static void free_server(uv_handle_t *handle) +{ + free(handle->data); +} diff --git a/src/nvim/os/server.h b/src/nvim/os/server.h new file mode 100644 index 0000000000..b9459a81af --- /dev/null +++ b/src/nvim/os/server.h @@ -0,0 +1,30 @@ +#ifndef NEOVIM_OS_SERVER_H +#define NEOVIM_OS_SERVER_H + +#include "os/channel_defs.h" + +/// Initializes the module +void server_init(); + +/// Teardown the server module +void server_teardown(); + +/// Starts listening on arbitrary tcp/unix addresses specified by +/// `endpoint` for API calls. The type of socket used(tcp or unix/pipe) will +/// be determined by parsing `endpoint`: If it's a valid tcp address in the +/// 'ip:port' format, then it will be tcp socket, else it will be a unix +/// socket or named pipe. +/// +/// @param endpoint Address of the server. Either a 'ip:port' string or an +/// arbitrary identifier(trimmed to 256 bytes) for the unix socket or +/// named pipe. +/// @param prot The rpc protocol to be used +void server_start(char *endpoint, ChannelProtocol prot); + +/// Stops listening on the address specified by `endpoint`. +/// +/// @param endpoint Address of the server. +void server_stop(char *endpoint); + +#endif // NEOVIM_OS_SERVER_H + diff --git a/src/nvim/os/server_defs.h b/src/nvim/os/server_defs.h new file mode 100644 index 0000000000..cbaefe3949 --- /dev/null +++ b/src/nvim/os/server_defs.h @@ -0,0 +1,7 @@ +#ifndef NEOVIM_OS_SERVER_DEFS_H +#define NEOVIM_OS_SERVER_DEFS_H + +typedef struct server Server; + +#endif // NEOVIM_OS_SERVER_DEFS_H + diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c new file mode 100644 index 0000000000..d14e355d19 --- /dev/null +++ b/src/nvim/os/shell.c @@ -0,0 +1,465 @@ +#include <string.h> +#include <stdbool.h> +#include <stdlib.h> + +#include <uv.h> + +#include "os/shell.h" +#include "os/signal.h" +#include "types.h" +#include "vim.h" +#include "message.h" +#include "ascii.h" +#include "memory.h" +#include "term.h" +#include "misc2.h" +#include "screen.h" +#include "memline.h" +#include "option_defs.h" +#include "charset.h" + +#define BUFFER_LENGTH 1024 + +typedef struct { + bool reading; + int old_state, old_mode, exit_status, exited; + char *wbuffer; + char rbuffer[BUFFER_LENGTH]; + uv_buf_t bufs[2]; + uv_stream_t *shell_stdin; + garray_T ga; +} ProcessData; + +/// Parses a command string into a sequence of words, taking quotes into +/// consideration. +/// +/// @param str The command string to be parsed +/// @param argv The vector that will be filled with copies of the parsed +/// words. It can be NULL if the caller only needs to count words. +/// @return The number of words parsed. +static int tokenize(char_u *str, char **argv); + +/// Calculates the length of a shell word. +/// +/// @param str A pointer to the first character of the word +/// @return The offset from `str` at which the word ends. +static int word_length(char_u *command); + +/// Queues selected range for writing to the child process stdin. +/// +/// @param req The structure containing information to peform the write +static void write_selection(uv_write_t *req); + +/// Cleanup memory and restore state modified by `os_call_shell`. +/// +/// @param data State shared by all functions collaborating with +/// `os_call_shell`. +/// @param opts Process spawning options, containing some allocated memory +/// @param shellopts Options passed to `os_call_shell`. Used for deciding +/// if/which messages are displayed. +static int proc_cleanup_exit(ProcessData *data, + uv_process_options_t *opts, + int shellopts); +// Callbacks for libuv +static void alloc_cb(uv_handle_t *handle, size_t suggested, uv_buf_t *buf); +static void read_cb(uv_stream_t *stream, ssize_t cnt, const uv_buf_t *buf); +static void write_cb(uv_write_t *req, int status); +static void exit_cb(uv_process_t *proc, int64_t status, int term_signal); + +char ** shell_build_argv(char_u *cmd, char_u *extra_shell_opt) +{ + int i; + char **rv; + int argc = tokenize(p_sh, NULL) + tokenize(p_shcf, NULL); + + rv = (char **)xmalloc((unsigned)((argc + 4) * sizeof(char *))); + + // Split 'shell' + i = tokenize(p_sh, rv); + + if (extra_shell_opt != NULL) { + // Push a copy of `extra_shell_opt` + rv[i++] = xstrdup((char *)extra_shell_opt); + } + + if (cmd != NULL) { + // Split 'shellcmdflag' + i += tokenize(p_shcf, rv + i); + rv[i++] = xstrdup((char *)cmd); + } + + rv[i] = NULL; + + return rv; +} + +void shell_free_argv(char **argv) +{ + char **p = argv; + + if (p == NULL) { + // Nothing was allocated, return + return; + } + + while (*p != NULL) { + // Free each argument + free(*p); + p++; + } + + free(argv); +} + +int os_call_shell(char_u *cmd, ShellOpts opts, char_u *extra_shell_arg) +{ + uv_stdio_container_t proc_stdio[3]; + uv_process_options_t proc_opts; + uv_process_t proc; + uv_pipe_t proc_stdin, proc_stdout; + uv_write_t write_req; + int expected_exits = 1; + ProcessData pdata = { + .reading = false, + .exited = 0, + .old_mode = cur_tmode, + .old_state = State, + .shell_stdin = (uv_stream_t *)&proc_stdin, + .wbuffer = NULL, + }; + + out_flush(); + if (opts & kShellOptCooked) { + // set to normal mode + settmode(TMODE_COOK); + } + + // While the child is running, ignore terminating signals + signal_reject_deadly(); + + // Create argv for `uv_spawn` + // TODO(tarruda): we can use a static buffer for small argument vectors. 1024 + // bytes should be enough for most of the commands and if more is necessary + // we can allocate a another buffer + proc_opts.args = shell_build_argv(cmd, extra_shell_arg); + proc_opts.file = proc_opts.args[0]; + proc_opts.exit_cb = exit_cb; + // Initialize libuv structures + proc_opts.stdio = proc_stdio; + proc_opts.stdio_count = 3; + // Hide window on Windows :) + proc_opts.flags = UV_PROCESS_WINDOWS_HIDE; + proc_opts.cwd = NULL; + proc_opts.env = NULL; + + // The default is to inherit all standard file descriptors(this will change + // when the UI is moved to an external process) + proc_stdio[0].flags = UV_INHERIT_FD; + proc_stdio[0].data.fd = 0; + proc_stdio[1].flags = UV_INHERIT_FD; + proc_stdio[1].data.fd = 1; + proc_stdio[2].flags = UV_INHERIT_FD; + proc_stdio[2].data.fd = 2; + + if (opts & (kShellOptHideMess | kShellOptExpand)) { + // Ignore the shell stdio(redirects to /dev/null on unixes) + proc_stdio[0].flags = UV_IGNORE; + proc_stdio[1].flags = UV_IGNORE; + proc_stdio[2].flags = UV_IGNORE; + } else { + State = EXTERNCMD; + + if (opts & kShellOptWrite) { + // Write from the current buffer into the process stdin + uv_pipe_init(uv_default_loop(), &proc_stdin, 0); + write_req.data = &pdata; + proc_stdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; + proc_stdio[0].data.stream = (uv_stream_t *)&proc_stdin; + } + + if (opts & kShellOptRead) { + // Read from the process stdout into the current buffer + uv_pipe_init(uv_default_loop(), &proc_stdout, 0); + proc_stdout.data = &pdata; + proc_stdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; + proc_stdio[1].data.stream = (uv_stream_t *)&proc_stdout; + ga_init(&pdata.ga, 1, BUFFER_LENGTH); + } + } + + if (uv_spawn(uv_default_loop(), &proc, &proc_opts)) { + // Failed, probably due to `sh` not being executable + if (!emsg_silent) { + MSG_PUTS(_("\nCannot execute shell ")); + msg_outtrans(p_sh); + msg_putchar('\n'); + } + + return proc_cleanup_exit(&pdata, &proc_opts, opts); + } + + // Assign the flag address after `proc` is initialized by `uv_spawn` + proc.data = &pdata; + + if (opts & kShellOptWrite) { + // Queue everything for writing to the shell stdin + write_selection(&write_req); + expected_exits++; + } + + if (opts & kShellOptRead) { + // Start the read stream for the shell stdout + uv_read_start((uv_stream_t *)&proc_stdout, alloc_cb, read_cb); + expected_exits++; + } + + // Keep running the loop until all three handles are completely closed + while (pdata.exited < expected_exits) { + uv_run(uv_default_loop(), UV_RUN_ONCE); + + if (got_int) { + // Forward SIGINT to the shell + // TODO(tarruda): for now this is only needed if the terminal is in raw + // mode, but when the UI is externalized we'll also need it, so leave it + // here + uv_process_kill(&proc, SIGINT); + got_int = false; + } + } + + if (opts & kShellOptRead) { + if (pdata.ga.ga_len > 0) { + // If there's an unfinished line in the growable array, append it now. + append_ga_line(&pdata.ga); + // remember that the NL was missing + curbuf->b_no_eol_lnum = curwin->w_cursor.lnum; + } else { + curbuf->b_no_eol_lnum = 0; + } + ga_clear(&pdata.ga); + } + + if (opts & kShellOptWrite) { + free(pdata.wbuffer); + } + + return proc_cleanup_exit(&pdata, &proc_opts, opts); +} + +static int tokenize(char_u *str, char **argv) +{ + int argc = 0, len; + char_u *p = str; + + while (*p != NUL) { + len = word_length(p); + + if (argv != NULL) { + // Fill the slot + argv[argc] = xmalloc(len + 1); + memcpy(argv[argc], p, len); + argv[argc][len] = NUL; + } + + argc++; + p += len; + p = skipwhite(p); + } + + return argc; +} + +static int word_length(char_u *str) +{ + char_u *p = str; + bool inquote = false; + int length = 0; + + // Move `p` to the end of shell word by advancing the pointer while it's + // inside a quote or it's a non-whitespace character + while (*p && (inquote || (*p != ' ' && *p != TAB))) { + if (*p == '"') { + // Found a quote character, switch the `inquote` flag + inquote = !inquote; + } + + p++; + length++; + } + + return length; +} + +/// To remain compatible with the old implementation(which forked a process +/// for writing) the entire text is copied to a temporary buffer before the +/// event loop starts. If we don't(by writing in chunks returned by `ml_get`) +/// the buffer being modified might get modified by reading from the process +/// before we finish writing. +static void write_selection(uv_write_t *req) +{ + ProcessData *pdata = (ProcessData *)req->data; + // TODO(tarruda): use a static buffer for up to a limit(BUFFER_LENGTH) and + // only after filled we should start allocating memory(skip unnecessary + // allocations for small writes) + int buflen = BUFFER_LENGTH; + pdata->wbuffer = (char *)xmalloc(buflen); + uv_buf_t uvbuf; + linenr_T lnum = curbuf->b_op_start.lnum; + int off = 0; + int written = 0; + char_u *lp = ml_get(lnum); + int l; + int len; + + for (;;) { + l = strlen((char *)lp + written); + if (l == 0) { + len = 0; + } else if (lp[written] == NL) { + // NL -> NUL translation + len = 1; + if (off + len >= buflen) { + // Resize the buffer + buflen *= 2; + pdata->wbuffer = xrealloc(pdata->wbuffer, buflen); + } + pdata->wbuffer[off++] = NUL; + } else { + char_u *s = vim_strchr(lp + written, NL); + len = s == NULL ? l : s - (lp + written); + while (off + len >= buflen) { + // Resize the buffer + buflen *= 2; + pdata->wbuffer = xrealloc(pdata->wbuffer, buflen); + } + memcpy(pdata->wbuffer + off, lp + written, len); + off += len; + } + if (len == l) { + // Finished a line, add a NL, unless this line + // should not have one. + // FIXME need to make this more readable + if (lnum != curbuf->b_op_end.lnum + || !curbuf->b_p_bin + || (lnum != curbuf->b_no_eol_lnum + && (lnum != + curbuf->b_ml.ml_line_count + || curbuf->b_p_eol))) { + if (off + 1 >= buflen) { + // Resize the buffer + buflen *= 2; + pdata->wbuffer = xrealloc(pdata->wbuffer, buflen); + } + pdata->wbuffer[off++] = NL; + } + ++lnum; + if (lnum > curbuf->b_op_end.lnum) { + break; + } + lp = ml_get(lnum); + written = 0; + } else if (len > 0) { + written += len; + } + } + + uvbuf.base = pdata->wbuffer; + uvbuf.len = off; + + uv_write(req, pdata->shell_stdin, &uvbuf, 1, write_cb); +} + +// "Allocates" a buffer for reading from the shell stdout. +static void alloc_cb(uv_handle_t *handle, size_t suggested, uv_buf_t *buf) +{ + ProcessData *pdata = (ProcessData *)handle->data; + + if (pdata->reading) { + buf->len = 0; + return; + } + + buf->base = pdata->rbuffer; + buf->len = BUFFER_LENGTH; + // Avoid `alloc_cb`, `alloc_cb` sequences on windows + pdata->reading = true; +} + +static void read_cb(uv_stream_t *stream, ssize_t cnt, const uv_buf_t *buf) +{ + // TODO(tarruda): avoid using a growable array for this, refactor the + // algorithm to call `ml_append` directly(skip unecessary copies/resizes) + int i; + ProcessData *pdata = (ProcessData *)stream->data; + + if (cnt <= 0) { + if (cnt != UV_ENOBUFS) { + uv_read_stop(stream); + uv_close((uv_handle_t *)stream, NULL); + pdata->exited++; + } + return; + } + + for (i = 0; i < cnt; ++i) { + if (pdata->rbuffer[i] == NL) { + // Insert the line + append_ga_line(&pdata->ga); + } else if (pdata->rbuffer[i] == NUL) { + // Translate NUL to NL + ga_append(&pdata->ga, NL); + } else { + // buffer data into the grow array + ga_append(&pdata->ga, pdata->rbuffer[i]); + } + } + + windgoto(msg_row, msg_col); + cursor_on(); + out_flush(); + + pdata->reading = false; +} + +static void write_cb(uv_write_t *req, int status) +{ + ProcessData *pdata = (ProcessData *)req->data; + uv_close((uv_handle_t *)pdata->shell_stdin, NULL); + pdata->exited++; +} + +static int proc_cleanup_exit(ProcessData *proc_data, + uv_process_options_t *proc_opts, + int shellopts) +{ + if (proc_data->exited) { + if (!emsg_silent && proc_data->exit_status != 0 && + !(shellopts & kShellOptSilent)) { + MSG_PUTS(_("\nshell returned ")); + msg_outnum((int64_t)proc_data->exit_status); + msg_putchar('\n'); + } + } + + State = proc_data->old_state; + + if (proc_data->old_mode == TMODE_RAW) { + // restore mode + settmode(TMODE_RAW); + } + + signal_accept_deadly(); + + // Release argv memory + shell_free_argv(proc_opts->args); + + return proc_data->exit_status; +} + +static void exit_cb(uv_process_t *proc, int64_t status, int term_signal) +{ + ProcessData *data = (ProcessData *)proc->data; + data->exited++; + data->exit_status = status; + uv_close((uv_handle_t *)proc, NULL); +} diff --git a/src/nvim/os/shell.h b/src/nvim/os/shell.h new file mode 100644 index 0000000000..776c36d384 --- /dev/null +++ b/src/nvim/os/shell.h @@ -0,0 +1,45 @@ +#ifndef NEOVIM_OS_SHELL_H +#define NEOVIM_OS_SHELL_H + +#include <stdbool.h> + +#include "types.h" + +// Flags for mch_call_shell() second argument +typedef enum { + kShellOptFilter = 1, ///< filtering text + kShellOptExpand = 2, ///< expanding wildcards + kShellOptCooked = 4, ///< set term to cooked mode + kShellOptDoOut = 8, ///< redirecting output + kShellOptSilent = 16, ///< don't print error returned by command + kShellOptRead = 32, ///< read lines and insert into buffer + kShellOptWrite = 64, ///< write lines from buffer + kShellOptHideMess = 128, ///< previously a global variable from os_unix.c +} ShellOpts; + +/// Builds the argument vector for running the shell configured in `sh` +/// ('shell' option), optionally with a command that will be passed with `shcf` +/// ('shellcmdflag'). +/// +/// @param cmd Command string. If NULL it will run an interactive shell. +/// @param extra_shell_opt Extra argument to the shell. If NULL it is ignored +/// @return A newly allocated argument vector. It must be freed with +/// `shell_free_argv` when no longer needed. +char ** shell_build_argv(char_u *cmd, char_u *extra_shell_arg); + +/// Releases the memory allocated by `shell_build_argv`. +/// +/// @param argv The argument vector. +void shell_free_argv(char **argv); + +/// Calls the user shell for running a command, interactive session or +/// wildcard expansion. It uses the shell set in the `sh` option. +/// +/// @param cmd The command to be executed. If NULL it will run an interactive +/// shell +/// @param opts Various options that control how the shell will work +/// @param extra_shell_arg Extra argument to be passed to the shell +int os_call_shell(char_u *cmd, ShellOpts opts, char_u *extra_shell_arg); + +#endif // NEOVIM_OS_SHELL_H + diff --git a/src/nvim/os/signal.c b/src/nvim/os/signal.c new file mode 100644 index 0000000000..d3bedad09c --- /dev/null +++ b/src/nvim/os/signal.c @@ -0,0 +1,163 @@ +#include <stdbool.h> + +#include <uv.h> + +#include "types.h" +#include "vim.h" +#include "globals.h" +#include "memline.h" +#include "eval.h" +#include "term.h" +#include "memory.h" +#include "misc1.h" +#include "misc2.h" +#include "os/event_defs.h" +#include "os/event.h" +#include "os/signal.h" + +static uv_signal_t sint, spipe, shup, squit, sterm, swinch; +#ifdef SIGPWR +static uv_signal_t spwr; +#endif + +static bool rejecting_deadly; +static char * signal_name(int signum); +static void deadly_signal(int signum); +static void signal_cb(uv_signal_t *, int signum); + +void signal_init() +{ + uv_signal_init(uv_default_loop(), &sint); + uv_signal_init(uv_default_loop(), &spipe); + uv_signal_init(uv_default_loop(), &shup); + uv_signal_init(uv_default_loop(), &squit); + uv_signal_init(uv_default_loop(), &sterm); + uv_signal_init(uv_default_loop(), &swinch); + uv_signal_start(&sint, signal_cb, SIGINT); + uv_signal_start(&spipe, signal_cb, SIGPIPE); + uv_signal_start(&shup, signal_cb, SIGHUP); + uv_signal_start(&squit, signal_cb, SIGQUIT); + uv_signal_start(&sterm, signal_cb, SIGTERM); + uv_signal_start(&swinch, signal_cb, SIGWINCH); +#ifdef SIGPWR + uv_signal_init(uv_default_loop(), &spwr); + uv_signal_start(&spwr, signal_cb, SIGPWR); +#endif +} + +void signal_stop() +{ + uv_signal_stop(&sint); + uv_signal_stop(&spipe); + uv_signal_stop(&shup); + uv_signal_stop(&squit); + uv_signal_stop(&sterm); + uv_signal_stop(&swinch); +#ifdef SIGPWR + uv_signal_stop(&spwr); +#endif +} + +void signal_reject_deadly() +{ + rejecting_deadly = true; +} + +void signal_accept_deadly() +{ + rejecting_deadly = false; +} + +void signal_handle(Event event) +{ + int signum = event.data.signum; + + switch (signum) { + case SIGINT: + got_int = true; + break; +#ifdef SIGPWR + case SIGPWR: + // Signal of a power failure(eg batteries low), flush the swap files to + // be safe + ml_sync_all(false, false); + break; +#endif + case SIGPIPE: + // Ignore + break; + case SIGWINCH: + shell_resized(); + break; + case SIGTERM: + case SIGQUIT: + case SIGHUP: + if (!rejecting_deadly) { + deadly_signal(signum); + } + break; + default: + fprintf(stderr, "Invalid signal %d", signum); + break; + } +} + +static char * signal_name(int signum) +{ + switch (signum) { + case SIGINT: + return "SIGINT"; +#ifdef SIGPWR + case SIGPWR: + return "SIGPWR"; +#endif + case SIGPIPE: + return "SIGPIPE"; + case SIGWINCH: + return "SIGWINCH"; + case SIGTERM: + return "SIGTERM"; + case SIGQUIT: + return "SIGQUIT"; + case SIGHUP: + return "SIGHUP"; + default: + return "Unknown"; + } +} + +// This function handles deadly signals. +// It tries to preserve any swap files and exit properly. +// (partly from Elvis). +// NOTE: Avoid unsafe functions, such as allocating memory, they can result in +// a deadlock. +static void deadly_signal(int signum) +{ + // Set the v:dying variable. + set_vim_var_nr(VV_DYING, 1); + + snprintf((char *)IObuff, sizeof(IObuff), "Vim: Caught deadly signal '%s'\n", + signal_name(signum)); + + // Preserve files and exit. + preserve_exit(); +} + +static void signal_cb(uv_signal_t *handle, int signum) +{ + if (rejecting_deadly) { + if (signum == SIGINT) { + got_int = true; + } + + return; + } + + Event event = { + .type = kEventSignal, + .data = { + .signum = signum + } + }; + event_push(event); +} diff --git a/src/nvim/os/signal.h b/src/nvim/os/signal.h new file mode 100644 index 0000000000..79fdc8d79c --- /dev/null +++ b/src/nvim/os/signal.h @@ -0,0 +1,13 @@ +#ifndef NEOVIM_OS_SIGNAL_H +#define NEOVIM_OS_SIGNAL_H + +#include "os/event_defs.h" + +void signal_init(void); +void signal_stop(void); +void signal_accept_deadly(void); +void signal_reject_deadly(void); +void signal_handle(Event event); + +#endif // NEOVIM_OS_SIGNAL_H + diff --git a/src/nvim/os/time.c b/src/nvim/os/time.c new file mode 100644 index 0000000000..1dc7ca68d4 --- /dev/null +++ b/src/nvim/os/time.c @@ -0,0 +1,86 @@ +#include <stdint.h> +#include <stdbool.h> +#include <sys/time.h> + +#include <uv.h> + +#include "os/time.h" +#include "vim.h" +#include "term.h" + +static uv_mutex_t delay_mutex; +static uv_cond_t delay_cond; + +static void microdelay(uint64_t ms); + +void time_init() +{ + uv_mutex_init(&delay_mutex); + uv_cond_init(&delay_cond); +} + +void os_delay(uint64_t milliseconds, bool ignoreinput) +{ + os_microdelay(milliseconds * 1000, ignoreinput); +} + +void os_microdelay(uint64_t microseconds, bool ignoreinput) +{ + int old_tmode; + + if (ignoreinput) { + // Go to cooked mode without echo, to allow SIGINT interrupting us + // here + old_tmode = curr_tmode; + + if (curr_tmode == TMODE_RAW) + settmode(TMODE_SLEEP); + + microdelay(microseconds); + + settmode(old_tmode); + } else { + microdelay(microseconds); + } +} + +static void microdelay(uint64_t microseconds) +{ + uint64_t hrtime; + int64_t ns = microseconds * 1000; // convert to nanoseconds + + uv_mutex_lock(&delay_mutex); + + while (ns > 0) { + hrtime = uv_hrtime(); + if (uv_cond_timedwait(&delay_cond, &delay_mutex, ns) == UV_ETIMEDOUT) + break; + ns -= uv_hrtime() - hrtime; + } + + uv_mutex_unlock(&delay_mutex); +} + +struct tm *os_localtime_r(const time_t *clock, struct tm *result) +{ +#ifdef UNIX + // POSIX provides localtime_r() as a thread-safe version of localtime(). + return localtime_r(clock, result); +#else + // Windows version of localtime() is thread-safe. + // See http://msdn.microsoft.com/en-us/library/bf12f0hc%28VS.80%29.aspx + struct tm *local_time = localtime(clock); // NOLINT + *result = *local_time; +return result; +#endif +} + +struct tm *os_get_localtime(struct tm *result) +{ + struct timeval tv; + if (gettimeofday(&tv, NULL) < 0) { + return NULL; + } + + return os_localtime_r(&tv.tv_sec, result); +} diff --git a/src/nvim/os/time.h b/src/nvim/os/time.h new file mode 100644 index 0000000000..ef795d03be --- /dev/null +++ b/src/nvim/os/time.h @@ -0,0 +1,35 @@ +#ifndef NEOVIM_OS_TIME_H +#define NEOVIM_OS_TIME_H + +#include <stdint.h> +#include <stdbool.h> + +/// Initializes the time module +void time_init(void); + +/// Sleeps for a certain amount of milliseconds +/// +/// @param milliseconds Number of milliseconds to sleep +/// @param ignoreinput If true, allow a SIGINT to interrupt us +void os_delay(uint64_t milliseconds, bool ignoreinput); + +/// Sleeps for a certain amount of microseconds +/// +/// @param microseconds Number of microseconds to sleep +/// @param ignoreinput If true, allow a SIGINT to interrupt us +void os_microdelay(uint64_t microseconds, bool ignoreinput); + +/// Portable version of POSIX localtime_r() +/// +/// @return NULL in case of error +struct tm *os_localtime_r(const time_t *clock, struct tm *result); + +/// Obtains the current UNIX timestamp and adjusts it to local time +/// +/// @param result Pointer to a 'struct tm' where the result should be placed +/// @return A pointer to a 'struct tm' in the current time zone (the 'result' +/// argument) or NULL in case of error +struct tm *os_get_localtime(struct tm *result); + +#endif // NEOVIM_OS_TIME_H + diff --git a/src/nvim/os/users.c b/src/nvim/os/users.c new file mode 100644 index 0000000000..e7b362637b --- /dev/null +++ b/src/nvim/os/users.c @@ -0,0 +1,87 @@ +// users.c -- operating system user information + +#include <uv.h> + +#include "os/os.h" +#include "garray.h" +#include "misc2.h" +#ifdef HAVE_PWD_H +# include <pwd.h> +#endif + +// Initialize users garray and fill it with os usernames. +// Return Ok for success, FAIL for failure. +int os_get_usernames(garray_T *users) +{ + if (users == NULL) { + return FAIL; + } + ga_init(users, sizeof(char *), 20); + +# if defined(HAVE_GETPWENT) && defined(HAVE_PWD_H) + char *user; + struct passwd *pw; + + setpwent(); + while ((pw = getpwent()) != NULL) { + // pw->pw_name shouldn't be NULL but just in case... + if (pw->pw_name != NULL) { + ga_grow(users, 1); + user = (char *)vim_strsave((char_u*)pw->pw_name); + if (user == NULL) { + return FAIL; + } + ((char **)(users->ga_data))[users->ga_len++] = user; + } + } + endpwent(); +# endif + + return OK; +} + +// Insert user name in s[len]. +// Return OK if a name found. +int os_get_user_name(char *s, size_t len) +{ + return os_get_uname(getuid(), s, len); +} + +// Insert user name for "uid" in s[len]. +// Return OK if a name found. +// If the name is not found, write the uid into s[len] and return FAIL. +int os_get_uname(uid_t uid, char *s, size_t len) +{ +#if defined(HAVE_PWD_H) && defined(HAVE_GETPWUID) + struct passwd *pw; + + if ((pw = getpwuid(uid)) != NULL + && pw->pw_name != NULL && *(pw->pw_name) != NUL) { + vim_strncpy((char_u *)s, (char_u *)pw->pw_name, len - 1); + return OK; + } +#endif + snprintf(s, len, "%d", (int)uid); + return FAIL; // a number is not a name +} + +// Returns the user directory for the given username. +// The caller has to free() the returned string. +// If the username is not found, NULL is returned. +char *os_get_user_directory(const char *name) +{ +#if defined(HAVE_GETPWNAM) && defined(HAVE_PWD_H) + struct passwd *pw; + if (name == NULL) { + return NULL; + } + pw = getpwnam(name); + if (pw != NULL) { + // save the string from the static passwd entry into malloced memory + char *user_directory = (char *)vim_strsave((char_u *)pw->pw_dir); + return user_directory; + } +#endif + return NULL; +} + diff --git a/src/nvim/os/uv_helpers.c b/src/nvim/os/uv_helpers.c new file mode 100644 index 0000000000..62b021de5e --- /dev/null +++ b/src/nvim/os/uv_helpers.c @@ -0,0 +1,70 @@ +#include <uv.h> + +#include "os/uv_helpers.h" +#include "vim.h" +#include "memory.h" + +/// Common structure that will always be assigned to the `data` field of +/// libuv handles. It has fields for many types of pointers, and allow a single +/// handle to contain data from many sources +typedef struct { + WStream *wstream; + RStream *rstream; + Job *job; +} HandleData; + +static HandleData *init(uv_handle_t *handle); + +RStream *handle_get_rstream(uv_handle_t *handle) +{ + RStream *rv = init(handle)->rstream; + assert(rv != NULL); + return rv; +} + +void handle_set_rstream(uv_handle_t *handle, RStream *rstream) +{ + init(handle)->rstream = rstream; +} + +WStream *handle_get_wstream(uv_handle_t *handle) +{ + WStream *rv = init(handle)->wstream; + assert(rv != NULL); + return rv; +} + +void handle_set_wstream(uv_handle_t *handle, WStream *wstream) +{ + HandleData *data = init(handle); + data->wstream = wstream; +} + +Job *handle_get_job(uv_handle_t *handle) +{ + Job *rv = init(handle)->job; + assert(rv != NULL); + return rv; +} + +void handle_set_job(uv_handle_t *handle, Job *job) +{ + init(handle)->job = job; +} + +static HandleData *init(uv_handle_t *handle) +{ + HandleData *rv; + + if (handle->data == NULL) { + rv = xmalloc(sizeof(HandleData)); + rv->rstream = NULL; + rv->wstream = NULL; + rv->job = NULL; + handle->data = rv; + } else { + rv = handle->data; + } + + return rv; +} diff --git a/src/nvim/os/uv_helpers.h b/src/nvim/os/uv_helpers.h new file mode 100644 index 0000000000..9d4cea30b2 --- /dev/null +++ b/src/nvim/os/uv_helpers.h @@ -0,0 +1,47 @@ +#ifndef NEOVIM_OS_UV_HELPERS_H +#define NEOVIM_OS_UV_HELPERS_H + +#include <uv.h> + +#include "os/wstream_defs.h" +#include "os/rstream_defs.h" +#include "os/job_defs.h" + +/// Gets the RStream instance associated with a libuv handle +/// +/// @param handle libuv handle +/// @return the RStream pointer +RStream *handle_get_rstream(uv_handle_t *handle); + +/// Associates a RStream instance with a libuv handle +/// +/// @param handle libuv handle +/// @param rstream the RStream pointer +void handle_set_rstream(uv_handle_t *handle, RStream *rstream); + +/// Gets the WStream instance associated with a libuv handle +/// +/// @param handle libuv handle +/// @return the WStream pointer +WStream *handle_get_wstream(uv_handle_t *handle); + +/// Associates a WStream instance with a libuv handle +/// +/// @param handle libuv handle +/// @param wstream the WStream pointer +void handle_set_wstream(uv_handle_t *handle, WStream *wstream); + +/// Gets the Job instance associated with a libuv handle +/// +/// @param handle libuv handle +/// @return the Job pointer +Job *handle_get_job(uv_handle_t *handle); + +/// Associates a Job instance with a libuv handle +/// +/// @param handle libuv handle +/// @param job the Job pointer +void handle_set_job(uv_handle_t *handle, Job *job); + +#endif // NEOVIM_OS_UV_HELPERS_H + diff --git a/src/nvim/os/wstream.c b/src/nvim/os/wstream.c new file mode 100644 index 0000000000..0b289e80f5 --- /dev/null +++ b/src/nvim/os/wstream.c @@ -0,0 +1,117 @@ +#include <stdint.h> +#include <stdbool.h> + +#include <uv.h> + +#include "os/uv_helpers.h" +#include "os/wstream.h" +#include "os/wstream_defs.h" +#include "vim.h" +#include "memory.h" + +struct wstream { + uv_stream_t *stream; + // Memory currently used by pending buffers + uint32_t curmem; + // Maximum memory used by this instance + uint32_t maxmem; + // Number of pending requests + uint32_t pending_reqs; + bool freed; +}; + +typedef struct { + WStream *wstream; + // Buffer containing data to be written + char *buffer; + // Size of the buffer + uint32_t length; + // If it's our responsibility to free the buffer + bool free; +} WriteData; + +static void write_cb(uv_write_t *req, int status); + +WStream * wstream_new(uint32_t maxmem) +{ + WStream *rv = xmalloc(sizeof(WStream)); + rv->maxmem = maxmem; + rv->stream = NULL; + rv->curmem = 0; + rv->pending_reqs = 0; + rv->freed = false; + + return rv; +} + +void wstream_free(WStream *wstream) +{ + if (!wstream->pending_reqs) { + free(wstream); + } else { + wstream->freed = true; + } +} + +void wstream_set_stream(WStream *wstream, uv_stream_t *stream) +{ + handle_set_wstream((uv_handle_t *)stream, wstream); + wstream->stream = stream; +} + +bool wstream_write(WStream *wstream, char *buffer, uint32_t length, bool free) +{ + WriteData *data; + uv_buf_t uvbuf; + uv_write_t *req; + + if (wstream->freed) { + // Don't accept write requests after the WStream instance was freed + return false; + } + + if (wstream->curmem + length > wstream->maxmem) { + return false; + } + + if (free) { + // We should only account for buffers that are ours to free + wstream->curmem += length; + } + + data = xmalloc(sizeof(WriteData)); + data->wstream = wstream; + data->buffer = buffer; + data->length = length; + data->free = free; + req = xmalloc(sizeof(uv_write_t)); + req->data = data; + uvbuf.base = buffer; + uvbuf.len = length; + wstream->pending_reqs++; + uv_write(req, wstream->stream, &uvbuf, 1, write_cb); + + return true; +} + +static void write_cb(uv_write_t *req, int status) +{ + WriteData *data = req->data; + + free(req); + + if (data->free) { + // Free the data written to the stream + free(data->buffer); + data->wstream->curmem -= data->length; + } + + data->wstream->pending_reqs--; + if (data->wstream->freed && data->wstream->pending_reqs == 0) { + // Last pending write, free the wstream; + free(data->wstream); + } + + free(data); +} + diff --git a/src/nvim/os/wstream.h b/src/nvim/os/wstream.h new file mode 100644 index 0000000000..4a557ffd9f --- /dev/null +++ b/src/nvim/os/wstream.h @@ -0,0 +1,40 @@ +#ifndef NEOVIM_OS_WSTREAM_H +#define NEOVIM_OS_WSTREAM_H + +#include <stdint.h> +#include <stdbool.h> +#include <uv.h> + +#include "os/wstream_defs.h" + +/// Creates a new WStream instance. A WStream encapsulates all the boilerplate +/// necessary for writing to a libuv stream. +/// +/// @param maxmem Maximum amount memory used by this `WStream` instance. +/// @return The newly-allocated `WStream` instance +WStream * wstream_new(uint32_t maxmem); + +/// Frees all memory allocated for a WStream instance +/// +/// @param wstream The `WStream` instance +void wstream_free(WStream *wstream); + +/// Sets the underlying `uv_stream_t` instance +/// +/// @param wstream The `WStream` instance +/// @param stream The new `uv_stream_t` instance +void wstream_set_stream(WStream *wstream, uv_stream_t *stream); + +/// Queues data for writing to the backing file descriptor of a `WStream` +/// instance. This will fail if the write would cause the WStream use more +/// memory than specified by `maxmem`. +/// +/// @param wstream The `WStream` instance +/// @param buffer The buffer which contains data to be written +/// @param length Number of bytes that should be written from `buffer` +/// @param free If true, `buffer` will be freed after the write is complete +/// @return true if the data was successfully queued, false otherwise. +bool wstream_write(WStream *wstream, char *buffer, uint32_t length, bool free); + +#endif // NEOVIM_OS_WSTREAM_H + diff --git a/src/nvim/os/wstream_defs.h b/src/nvim/os/wstream_defs.h new file mode 100644 index 0000000000..59d57365fa --- /dev/null +++ b/src/nvim/os/wstream_defs.h @@ -0,0 +1,7 @@ +#ifndef NEOVIM_OS_WSTREAM_DEFS_H +#define NEOVIM_OS_WSTREAM_DEFS_H + +typedef struct wstream WStream; + +#endif // NEOVIM_OS_WSTREAM_DEFS_H + |