diff options
Diffstat (limited to 'src/nvim/os')
-rw-r--r-- | src/nvim/os/env.c | 62 | ||||
-rw-r--r-- | src/nvim/os/fileio.c | 318 | ||||
-rw-r--r-- | src/nvim/os/fileio.h | 72 | ||||
-rw-r--r-- | src/nvim/os/fs.c | 511 | ||||
-rw-r--r-- | src/nvim/os/input.c | 79 | ||||
-rw-r--r-- | src/nvim/os/os_defs.h | 8 | ||||
-rw-r--r-- | src/nvim/os/pty_process.h | 9 | ||||
-rw-r--r-- | src/nvim/os/pty_process_unix.c | 245 | ||||
-rw-r--r-- | src/nvim/os/pty_process_unix.h | 31 | ||||
-rw-r--r-- | src/nvim/os/pty_process_win.h | 28 | ||||
-rw-r--r-- | src/nvim/os/shell.c | 284 | ||||
-rw-r--r-- | src/nvim/os/signal.c | 25 | ||||
-rw-r--r-- | src/nvim/os/stdpaths.c | 54 | ||||
-rw-r--r-- | src/nvim/os/time.c | 42 | ||||
-rw-r--r-- | src/nvim/os/users.c | 1 |
15 files changed, 1584 insertions, 185 deletions
diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c index edc430410c..747a34d8ce 100644 --- a/src/nvim/os/env.c +++ b/src/nvim/os/env.c @@ -12,7 +12,6 @@ #include "nvim/os/os.h" #include "nvim/memory.h" #include "nvim/message.h" -#include "nvim/misc2.h" #include "nvim/path.h" #include "nvim/strings.h" #include "nvim/eval.h" @@ -113,11 +112,11 @@ int64_t os_get_pid(void) #endif } -/// Get the hostname of the machine running Neovim. +/// Gets the hostname of the current machine. /// -/// @param hostname Buffer to store the hostname. -/// @param len Length of `hostname`. -void os_get_hostname(char *hostname, size_t len) +/// @param hostname Buffer to store the hostname. +/// @param size Size of `hostname`. +void os_get_hostname(char *hostname, size_t size) { #ifdef HAVE_SYS_UTSNAME_H struct utsname vutsname; @@ -125,8 +124,7 @@ void os_get_hostname(char *hostname, size_t len) if (uname(&vutsname) < 0) { *hostname = '\0'; } else { - strncpy(hostname, vutsname.nodename, len - 1); - hostname[len - 1] = '\0'; + xstrlcpy(hostname, vutsname.nodename, size); } #else // TODO(unknown): Implement this for windows. @@ -227,25 +225,24 @@ char_u *expand_env_save_opt(char_u *src, bool one) /// "~/" is also expanded, using $HOME. For Unix "~user/" is expanded. /// Skips over "\ ", "\~" and "\$" (not for Win32 though). /// If anything fails no expansion is done and dst equals src. -/// @param src Input string e.g. "$HOME/vim.hlp" -/// @param dst Where to put the result -/// @param dstlen Maximum length of the result +/// +/// @param src Input string e.g. "$HOME/vim.hlp" +/// @param dst[out] Where to put the result +/// @param dstlen Maximum length of the result void expand_env(char_u *src, char_u *dst, int dstlen) { expand_env_esc(src, dst, dstlen, false, false, NULL); } /// Expand environment variable with path name and escaping. -/// "~/" is also expanded, using $HOME. For Unix "~user/" is expanded. -/// Skips over "\ ", "\~" and "\$" (not for Win32 though). -/// If anything fails no expansion is done and dst equals src. -/// prefix recognize the start of a new name, for '~' expansion. -/// @param srcp Input string e.g. "$HOME/vim.hlp" -/// @param dst Where to put the result -/// @param dstlen Maximum length of the result -/// @param esc Should we escape spaces in expanded variables? -/// @param one Should we expand more than one '~'? -/// @param prefix Common prefix for paths, can be NULL +/// @see expand_env +/// +/// @param srcp Input string e.g. "$HOME/vim.hlp" +/// @param dst[out] Where to put the result +/// @param dstlen Maximum length of the result +/// @param esc Escape spaces in expanded variables +/// @param one `srcp` is a single filename +/// @param prefix Start again after this (can be NULL) void expand_env_esc(char_u *restrict srcp, char_u *restrict dst, int dstlen, @@ -561,8 +558,8 @@ const void *vim_colon_env_iter_rev(const char *const val, /// Vim's version of getenv(). /// Special handling of $HOME, $VIM and $VIMRUNTIME, allowing the user to /// override the vim runtime directory at runtime. Also does ACP to 'enc' -/// conversion for Win32. Results must be freed by the calling function. -/// @param name Name of environment variable to expand +/// conversion for Win32. Result must be freed by the caller. +/// @param name Environment variable to expand char *vim_getenv(const char *name) { const char *kos_env_path = os_getenv(name); @@ -599,6 +596,27 @@ char *vim_getenv(const char *name) if (p_hf != NULL && vim_strchr(p_hf, '$') == NULL) { vim_path = (char *)p_hf; } + +#ifdef WIN32 + // Find runtime path relative to the nvim binary i.e. ../share/runtime + if (vim_path == NULL) { + char exe_name[MAXPATHL]; + size_t exe_name_len = MAXPATHL; + if (os_exepath(exe_name, &exe_name_len) == 0) { + char *path_end = (char *)path_tail_with_sep((char_u *)exe_name); + *path_end = '\0'; // remove the trailing "nvim.exe" + path_end = (char *)path_tail((char_u *)exe_name); + *path_end = '\0'; // remove the trailing "bin/" + if (append_path( + exe_name, + "share" _PATHSEPSTR "nvim" _PATHSEPSTR "runtime" _PATHSEPSTR, + MAXPATHL) == OK) { + vim_path = exe_name; + } + } + } +#endif + if (vim_path != NULL) { // remove the file name char *vim_path_end = (char *)path_tail((char_u *)vim_path); diff --git a/src/nvim/os/fileio.c b/src/nvim/os/fileio.c new file mode 100644 index 0000000000..cf5bfd60ae --- /dev/null +++ b/src/nvim/os/fileio.c @@ -0,0 +1,318 @@ +/// @file fileio.c +/// +/// Buffered reading/writing to a file. Unlike fileio.c this is not dealing with +/// Neovim stuctures for buffer, with autocommands, etc: just fopen/fread/fwrite +/// replacement. + +#include <assert.h> +#include <stddef.h> +#include <stdbool.h> +#include <fcntl.h> + +#include "auto/config.h" + +#ifdef HAVE_SYS_UIO_H +# include <sys/uio.h> +#endif + +#include <uv.h> + +#include "nvim/os/fileio.h" +#include "nvim/memory.h" +#include "nvim/os/os.h" +#include "nvim/globals.h" +#include "nvim/rbuffer.h" +#include "nvim/macros.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/fileio.c.generated.h" +#endif + +/// Open file +/// +/// @param[out] ret_fp Address where information needed for reading from or +/// writing to a file is saved +/// @param[in] fname File name to open. +/// @param[in] flags Flags, @see FileOpenFlags. Currently reading from and +/// writing to the file at once is not supported, so either +/// FILE_WRITE_ONLY or FILE_READ_ONLY is required. +/// @param[in] mode Permissions for the newly created file (ignored if flags +/// does not have FILE_CREATE\*). +/// +/// @return Error code (@see os_strerror()) or 0. +int file_open(FileDescriptor *const ret_fp, const char *const fname, + const int flags, const int mode) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + int os_open_flags = 0; + int fd; + TriState wr = kNone; +#define FLAG(flags, flag, fcntl_flags, wrval, cond) \ + do { \ + if (flags & flag) { \ + os_open_flags |= fcntl_flags; \ + assert(cond); \ + if (wrval != kNone) { \ + wr = wrval; \ + } \ + } \ + } while (0) + FLAG(flags, kFileWriteOnly, O_WRONLY, kTrue, true); + FLAG(flags, kFileCreateOnly, O_CREAT|O_EXCL|O_WRONLY, kTrue, true); + FLAG(flags, kFileCreate, O_CREAT|O_WRONLY, kTrue, !(flags & kFileCreateOnly)); + FLAG(flags, kFileTruncate, O_TRUNC|O_WRONLY, kTrue, + !(flags & kFileCreateOnly)); + FLAG(flags, kFileReadOnly, O_RDONLY, kFalse, wr != kTrue); +#ifdef O_NOFOLLOW + FLAG(flags, kFileNoSymlink, O_NOFOLLOW, kNone, true); +#endif +#undef FLAG + + fd = os_open(fname, os_open_flags, mode); + + if (fd < 0) { + return fd; + } + + ret_fp->wr = (wr == kTrue); + ret_fp->fd = fd; + ret_fp->eof = false; + ret_fp->rv = rbuffer_new(kRWBufferSize); + ret_fp->_error = 0; + if (ret_fp->wr) { + ret_fp->rv->data = ret_fp; + ret_fp->rv->full_cb = (rbuffer_callback)&file_rb_write_full_cb; + } + return 0; +} + +/// Like file_open(), but allocate and return ret_fp +/// +/// @param[out] error Error code, @see os_strerror(). Is set to zero on +/// success. +/// @param[in] fname File name to open. +/// @param[in] flags Flags, @see FileOpenFlags. +/// @param[in] mode Permissions for the newly created file (ignored if flags +/// does not have FILE_CREATE\*). +/// +/// @return [allocated] Opened file or NULL in case of error. +FileDescriptor *file_open_new(int *const error, const char *const fname, + const int flags, const int mode) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_MALLOC FUNC_ATTR_WARN_UNUSED_RESULT +{ + FileDescriptor *const fp = xmalloc(sizeof(*fp)); + if ((*error = file_open(fp, fname, flags, mode)) != 0) { + xfree(fp); + return NULL; + } + return fp; +} + +/// Close file and free its buffer +/// +/// @param[in,out] fp File to close. +/// +/// @return 0 or error code. +int file_close(FileDescriptor *const fp) FUNC_ATTR_NONNULL_ALL +{ + const int error = file_fsync(fp); + const int error2 = os_close(fp->fd); + rbuffer_free(fp->rv); + if (error2 != 0) { + return error2; + } + return error; +} + +/// Close and free file obtained using file_open_new() +/// +/// @param[in,out] fp File to close. +/// +/// @return 0 or error code. +int file_free(FileDescriptor *const fp) FUNC_ATTR_NONNULL_ALL +{ + const int ret = file_close(fp); + xfree(fp); + return ret; +} + +/// Flush file modifications to disk +/// +/// @param[in,out] fp File to work with. +/// +/// @return 0 or error code. +int file_fsync(FileDescriptor *const fp) + FUNC_ATTR_NONNULL_ALL +{ + if (!fp->wr) { + return 0; + } + file_rb_write_full_cb(fp->rv, fp); + if (fp->_error != 0) { + const int error = fp->_error; + fp->_error = 0; + return error; + } + return os_fsync(fp->fd); +} + +/// Buffer used for writing +/// +/// Like IObuff, but allows file_\* callers not to care about spoiling it. +static char writebuf[kRWBufferSize]; + +/// Function run when RBuffer is full when writing to a file +/// +/// Actually does writing to the file, may also be invoked directly. +/// +/// @param[in,out] rv RBuffer instance used. +/// @param[in,out] fp File to work with. +static void file_rb_write_full_cb(RBuffer *const rv, FileDescriptor *const fp) + FUNC_ATTR_NONNULL_ALL +{ + assert(fp->wr); + assert(rv->data == (void *)fp); + if (rbuffer_size(rv) == 0) { + return; + } + const size_t read_bytes = rbuffer_read(rv, writebuf, kRWBufferSize); + const ptrdiff_t wres = os_write(fp->fd, writebuf, read_bytes); + if (wres != (ptrdiff_t)read_bytes) { + if (wres >= 0) { + fp->_error = UV_EIO; + } else { + fp->_error = (int)wres; + } + } +} + +/// Read from file +/// +/// @param[in,out] fp File to work with. +/// @param[out] ret_buf Buffer to read to. Must not be NULL. +/// @param[in] size Number of bytes to read. Buffer must have at least ret_buf +/// bytes. +/// +/// @return error_code (< 0) or number of bytes read. +ptrdiff_t file_read(FileDescriptor *const fp, char *const ret_buf, + const size_t size) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + assert(!fp->wr); + char *buf = ret_buf; + size_t read_remaining = size; + RBuffer *const rv = fp->rv; + while (read_remaining) { + const size_t rv_size = rbuffer_size(rv); + if (rv_size > 0) { + const size_t rsize = rbuffer_read(rv, buf, MIN(rv_size, read_remaining)); + buf += rsize; + read_remaining -= rsize; + } + if (fp->eof) { + break; + } + if (read_remaining) { + assert(rbuffer_size(rv) == 0); + rbuffer_reset(rv); +#ifdef HAVE_READV + // If there is readv() syscall, then take an opportunity to populate + // both target buffer and RBuffer at once, … + size_t write_count; + struct iovec iov[] = { + { .iov_base = buf, .iov_len = read_remaining }, + { .iov_base = rbuffer_write_ptr(rv, &write_count), + .iov_len = kRWBufferSize }, + }; + assert(write_count == kRWBufferSize); + const ptrdiff_t r_ret = os_readv(fp->fd, &fp->eof, iov, + ARRAY_SIZE(iov)); + if (r_ret > 0) { + if (r_ret > (ptrdiff_t)read_remaining) { + rbuffer_produced(rv, (size_t)(r_ret - (ptrdiff_t)read_remaining)); + read_remaining = 0; + } else { + buf += (size_t)r_ret; + read_remaining -= (size_t)r_ret; + } + } else if (r_ret < 0) { + return r_ret; + } +#else + if (read_remaining >= kRWBufferSize) { + // …otherwise leave RBuffer empty and populate only target buffer, + // because filtering information through rbuffer will be more syscalls. + const ptrdiff_t r_ret = os_read(fp->fd, &fp->eof, buf, read_remaining); + if (r_ret >= 0) { + read_remaining -= (size_t)r_ret; + return (ptrdiff_t)(size - read_remaining); + } else if (r_ret < 0) { + return r_ret; + } + } else { + size_t write_count; + const ptrdiff_t r_ret = os_read(fp->fd, &fp->eof, + rbuffer_write_ptr(rv, &write_count), + kRWBufferSize); + assert(write_count == kRWBufferSize); + if (r_ret > 0) { + rbuffer_produced(rv, (size_t)r_ret); + } else if (r_ret < 0) { + return r_ret; + } + } +#endif + } + } + return (ptrdiff_t)(size - read_remaining); +} + +/// Write to a file +/// +/// @param[in] fd File descriptor to write to. +/// @param[in] buf Data to write. May be NULL if size is zero. +/// @param[in] size Amount of bytes to write. +/// +/// @return Number of bytes written or libuv error code (< 0). +ptrdiff_t file_write(FileDescriptor *const fp, const char *const buf, + const size_t size) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ARG(1) +{ + assert(fp->wr); + const size_t written = rbuffer_write(fp->rv, buf, size); + if (fp->_error != 0) { + const int error = fp->_error; + fp->_error = 0; + return error; + } else if (written != size) { + return UV_EIO; + } + return (ptrdiff_t)written; +} + +/// Buffer used for skipping. Its contents is undefined and should never be +/// used. +static char skipbuf[kRWBufferSize]; + +/// Skip some bytes +/// +/// This is like `fseek(fp, size, SEEK_CUR)`, but actual implementation simply +/// reads to a buffer and discards the result. +ptrdiff_t file_skip(FileDescriptor *const fp, const size_t size) + FUNC_ATTR_NONNULL_ALL +{ + assert(!fp->wr); + size_t read_bytes = 0; + do { + const ptrdiff_t new_read_bytes = file_read( + fp, skipbuf, MIN(size - read_bytes, sizeof(skipbuf))); + if (new_read_bytes < 0) { + return new_read_bytes; + } else if (new_read_bytes == 0) { + break; + } + read_bytes += (size_t)new_read_bytes; + } while (read_bytes < size && !file_eof(fp)); + + return (ptrdiff_t)read_bytes; +} diff --git a/src/nvim/os/fileio.h b/src/nvim/os/fileio.h new file mode 100644 index 0000000000..2cffd5c851 --- /dev/null +++ b/src/nvim/os/fileio.h @@ -0,0 +1,72 @@ +#ifndef NVIM_OS_FILEIO_H +#define NVIM_OS_FILEIO_H + +#include <stdbool.h> +#include <stddef.h> + +#include "nvim/func_attr.h" +#include "nvim/rbuffer.h" + +/// Structure used to read from/write to file +typedef struct { + int fd; ///< File descriptor. + int _error; ///< Error code for use with RBuffer callbacks or zero. + RBuffer *rv; ///< Read or write buffer. + bool wr; ///< True if file is in write mode. + bool eof; ///< True if end of file was encountered. +} FileDescriptor; + +/// file_open() flags +typedef enum { + kFileReadOnly = 1, ///< Open file read-only. Default. + kFileCreate = 2, ///< Create file if it does not exist yet. + ///< Implies kFileWriteOnly. + kFileWriteOnly = 4, ///< Open file for writing only. + ///< Cannot be used with kFileReadOnly. + kFileNoSymlink = 8, ///< Do not allow symbolic links. + kFileCreateOnly = 16, ///< Only create the file, failing if it already + ///< exists. Implies kFileWriteOnly. Cannot be used + ///< with kFileCreate. + kFileTruncate = 32, ///< Truncate the file if it exists. + ///< Implies kFileWriteOnly. Cannot be used with + ///< kFileCreateOnly. +} FileOpenFlags; + +static inline bool file_eof(const FileDescriptor *const fp) + REAL_FATTR_PURE REAL_FATTR_WARN_UNUSED_RESULT REAL_FATTR_NONNULL_ALL; + +/// Check whether end of file was encountered +/// +/// @param[in] fp File to check. +/// +/// @return true if it was, false if it was not or read operation was never +/// performed. +static inline bool file_eof(const FileDescriptor *const fp) +{ + return fp->eof && rbuffer_size(fp->rv) == 0; +} + +static inline int file_fd(const FileDescriptor *const fp) + REAL_FATTR_PURE REAL_FATTR_WARN_UNUSED_RESULT REAL_FATTR_NONNULL_ALL; + +/// Return the file descriptor associated with the FileDescriptor structure +/// +/// @param[in] fp File to check. +/// +/// @return File descriptor. +static inline int file_fd(const FileDescriptor *const fp) +{ + return fp->fd; +} + +enum { + /// Read or write buffer size + /// + /// Currently equal to (IOSIZE - 1), but they do not need to be connected. + kRWBufferSize = 1024 +}; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/fileio.h.generated.h" +#endif +#endif // NVIM_OS_FILEIO_H diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c index 143a7160b0..30e08ac129 100644 --- a/src/nvim/os/fs.c +++ b/src/nvim/os/fs.c @@ -1,23 +1,51 @@ // fs.c -- filesystem access #include <stdbool.h> - +#include <stddef.h> #include <assert.h> +#include <limits.h> #include <fcntl.h> +#include <errno.h> + +#include "auto/config.h" + +#ifdef HAVE_SYS_UIO_H +# include <sys/uio.h> +#endif + +#include <uv.h> #include "nvim/os/os.h" #include "nvim/os/os_defs.h" #include "nvim/ascii.h" #include "nvim/memory.h" #include "nvim/message.h" +#include "nvim/assert.h" #include "nvim/misc1.h" -#include "nvim/misc2.h" #include "nvim/path.h" #include "nvim/strings.h" +#ifdef WIN32 +#include "nvim/mbyte.h" // for utf8_to_utf16, utf16_to_utf8 +#endif + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "os/fs.c.generated.h" #endif +#define RUN_UV_FS_FUNC(ret, func, ...) \ + do { \ + bool did_try_to_free = false; \ +uv_call_start: {} \ + uv_fs_t req; \ + ret = func(&fs_loop, &req, __VA_ARGS__); \ + uv_fs_req_cleanup(&req); \ + if (ret == UV_ENOMEM && !did_try_to_free) { \ + try_to_free_memory(); \ + did_try_to_free = true; \ + goto uv_call_start; \ + } \ + } while (0) + // Many fs functions from libuv return that value on success. static const int kLibuvSuccess = 0; static uv_loop_t fs_loop; @@ -135,8 +163,12 @@ int os_nodetype(const char *name) // saves us the hassle. int nodetype = NODE_WRITABLE; - int fd = os_open(name, O_RDONLY, 0); - switch(uv_guess_handle(fd)) { + int fd = os_open(name, O_RDONLY +#ifdef O_NONBLOCK + | O_NONBLOCK +#endif + , 0); + switch (uv_guess_handle(fd)) { case UV_TTY: // FILE_TYPE_CHAR nodetype = NODE_WRITABLE; break; @@ -160,6 +192,18 @@ int os_nodetype(const char *name) return nodetype; } +/// Gets the absolute path of the currently running executable. +/// +/// @param[out] buffer Returns the path string. +/// @param[in] size Size of `buffer`. +/// +/// @return `0` on success, or libuv error code on failure. +int os_exepath(char *buffer, size_t *size) + FUNC_ATTR_NONNULL_ALL +{ + return uv_exepath(buffer, size); +} + /// Checks if the given path represents an executable file. /// /// @param[in] name Name of the executable. @@ -325,13 +369,168 @@ static bool is_executable_in_path(const char_u *name, char_u **abspath) int os_open(const char* path, int flags, int mode) FUNC_ATTR_NONNULL_ALL { - uv_fs_t open_req; - int r = uv_fs_open(&fs_loop, &open_req, path, flags, mode, NULL); - uv_fs_req_cleanup(&open_req); - // r is the same as open_req.result (except for OOM: then only r is set). + int r; + RUN_UV_FS_FUNC(r, uv_fs_open, path, flags, mode, NULL); return r; } +/// Close a file +/// +/// @return 0 or libuv error code on failure. +int os_close(const int fd) +{ + int r; + RUN_UV_FS_FUNC(r, uv_fs_close, fd, NULL); + return r; +} + +/// Read from a file +/// +/// Handles EINTR and ENOMEM, but not other errors. +/// +/// @param[in] fd File descriptor to read from. +/// @param[out] ret_eof Is set to true if EOF was encountered, otherwise set +/// to false. Initial value is ignored. +/// @param[out] ret_buf Buffer to write to. May be NULL if size is zero. +/// @param[in] size Amount of bytes to read. +/// +/// @return Number of bytes read or libuv error code (< 0). +ptrdiff_t os_read(const int fd, bool *ret_eof, char *const ret_buf, + const size_t size) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + *ret_eof = false; + if (ret_buf == NULL) { + assert(size == 0); + return 0; + } + size_t read_bytes = 0; + bool did_try_to_free = false; + while (read_bytes != size) { + const ptrdiff_t cur_read_bytes = read(fd, ret_buf + read_bytes, + size - read_bytes); + if (cur_read_bytes > 0) { + read_bytes += (size_t)cur_read_bytes; + assert(read_bytes <= size); + } + if (cur_read_bytes < 0) { + const int error = os_translate_sys_error(errno); + errno = 0; + if (error == UV_EINTR || error == UV_EAGAIN) { + continue; + } else if (error == UV_ENOMEM && !did_try_to_free) { + try_to_free_memory(); + did_try_to_free = true; + continue; + } else { + return (ptrdiff_t)error; + } + } + if (cur_read_bytes == 0) { + *ret_eof = true; + break; + } + } + return (ptrdiff_t)read_bytes; +} + +#ifdef HAVE_READV +/// Read from a file to multiple buffers at once +/// +/// Wrapper for readv(). +/// +/// @param[in] fd File descriptor to read from. +/// @param[out] ret_eof Is set to true if EOF was encountered, otherwise set +/// to false. Initial value is ignored. +/// @param[out] iov Description of buffers to write to. Note: this description +/// may change, it is incorrect to use data it points to after +/// os_readv(). +/// @param[in] iov_size Number of buffers in iov. +ptrdiff_t os_readv(int fd, bool *ret_eof, struct iovec *iov, size_t iov_size) + FUNC_ATTR_NONNULL_ALL +{ + *ret_eof = false; + size_t read_bytes = 0; + bool did_try_to_free = false; + size_t toread = 0; + for (size_t i = 0; i < iov_size; i++) { + // Overflow, trying to read too much data + assert(toread <= SIZE_MAX - iov[i].iov_len); + toread += iov[i].iov_len; + } + while (read_bytes < toread && iov_size && !*ret_eof) { + ptrdiff_t cur_read_bytes = readv(fd, iov, (int)iov_size); + if (toread && cur_read_bytes == 0) { + *ret_eof = true; + } + if (cur_read_bytes > 0) { + read_bytes += (size_t)cur_read_bytes; + while (iov_size && cur_read_bytes) { + if (cur_read_bytes < (ptrdiff_t)iov->iov_len) { + iov->iov_len -= (size_t)cur_read_bytes; + iov->iov_base = (char *)iov->iov_base + cur_read_bytes; + cur_read_bytes = 0; + } else { + cur_read_bytes -= (ptrdiff_t)iov->iov_len; + iov_size--; + iov++; + } + } + } else if (cur_read_bytes < 0) { + const int error = os_translate_sys_error(errno); + errno = 0; + if (error == UV_EINTR || error == UV_EAGAIN) { + continue; + } else if (error == UV_ENOMEM && !did_try_to_free) { + try_to_free_memory(); + did_try_to_free = true; + continue; + } else { + return (ptrdiff_t)error; + } + } + } + return (ptrdiff_t)read_bytes; +} +#endif // HAVE_READV + +/// Write to a file +/// +/// @param[in] fd File descriptor to write to. +/// @param[in] buf Data to write. May be NULL if size is zero. +/// @param[in] size Amount of bytes to write. +/// +/// @return Number of bytes written or libuv error code (< 0). +ptrdiff_t os_write(const int fd, const char *const buf, const size_t size) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (buf == NULL) { + assert(size == 0); + return 0; + } + size_t written_bytes = 0; + while (written_bytes != size) { + const ptrdiff_t cur_written_bytes = write(fd, buf + written_bytes, + size - written_bytes); + if (cur_written_bytes > 0) { + written_bytes += (size_t)cur_written_bytes; + } + if (cur_written_bytes < 0) { + const int error = os_translate_sys_error(errno); + errno = 0; + if (error == UV_EINTR || error == UV_EAGAIN) { + continue; + } else { + return error; + } + } + if (cur_written_bytes == 0) { + return UV_UNKNOWN; + } + } + return (ptrdiff_t)written_bytes; +} + /// Flushes file modifications to disk. /// /// @param fd the file descriptor of the file to flush to disk. @@ -339,9 +538,8 @@ int os_open(const char* path, int flags, int mode) /// @return `0` on success, a libuv error code on failure. int os_fsync(int fd) { - uv_fs_t fsync_req; - int r = uv_fs_fsync(&fs_loop, &fsync_req, fd, NULL); - uv_fs_req_cleanup(&fsync_req); + int r; + RUN_UV_FS_FUNC(r, uv_fs_fsync, fd, NULL); return r; } @@ -379,16 +577,9 @@ int32_t os_getperm(const char_u *name) int os_setperm(const char_u *name, int perm) FUNC_ATTR_NONNULL_ALL { - uv_fs_t request; - int result = uv_fs_chmod(&fs_loop, &request, - (const char*)name, perm, NULL); - uv_fs_req_cleanup(&request); - - if (result == kLibuvSuccess) { - return OK; - } - - return FAIL; + int r; + RUN_UV_FS_FUNC(r, uv_fs_chmod, (const char *)name, perm, NULL); + return (r == kLibuvSuccess ? OK : FAIL); } /// Changes the ownership of the file referred to by the open file descriptor. @@ -397,24 +588,21 @@ int os_setperm(const char_u *name, int perm) /// /// @note If the `owner` or `group` is specified as `-1`, then that ID is not /// changed. -int os_fchown(int file_descriptor, uv_uid_t owner, uv_gid_t group) - FUNC_ATTR_NONNULL_ALL +int os_fchown(int fd, uv_uid_t owner, uv_gid_t group) { - uv_fs_t request; - int result = uv_fs_fchown(&fs_loop, &request, file_descriptor, - owner, group, NULL); - uv_fs_req_cleanup(&request); - return result; + int r; + RUN_UV_FS_FUNC(r, uv_fs_fchown, fd, owner, group, NULL); + return r; } -/// Check if a file exists. +/// Check if a path exists. /// -/// @return `true` if `name` exists. -bool os_file_exists(const char_u *name) +/// @return `true` if `path` exists +bool os_path_exists(const char_u *path) FUNC_ATTR_NONNULL_ALL { uv_stat_t statbuf; - return os_stat((char *)name, &statbuf) == kLibuvSuccess; + return os_stat((char *)path, &statbuf) == kLibuvSuccess; } /// Check if a file is readable. @@ -423,9 +611,8 @@ bool os_file_exists(const char_u *name) bool os_file_is_readable(const char *name) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { - uv_fs_t req; - int r = uv_fs_access(&fs_loop, &req, name, R_OK, NULL); - uv_fs_req_cleanup(&req); + int r; + RUN_UV_FS_FUNC(r, uv_fs_access, name, R_OK, NULL); return (r == 0); } @@ -437,9 +624,8 @@ bool os_file_is_readable(const char *name) int os_file_is_writable(const char *name) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { - uv_fs_t req; - int r = uv_fs_access(&fs_loop, &req, name, W_OK, NULL); - uv_fs_req_cleanup(&req); + int r; + RUN_UV_FS_FUNC(r, uv_fs_access, name, W_OK, NULL); if (r == 0) { return os_isdir((char_u *)name) ? 2 : 1; } @@ -452,16 +638,10 @@ int os_file_is_writable(const char *name) int os_rename(const char_u *path, const char_u *new_path) FUNC_ATTR_NONNULL_ALL { - uv_fs_t request; - int result = uv_fs_rename(&fs_loop, &request, - (const char *)path, (const char *)new_path, NULL); - uv_fs_req_cleanup(&request); - - if (result == kLibuvSuccess) { - return OK; - } - - return FAIL; + int r; + RUN_UV_FS_FUNC(r, uv_fs_rename, (const char *)path, (const char *)new_path, + NULL); + return (r == kLibuvSuccess ? OK : FAIL); } /// Make a directory. @@ -470,10 +650,9 @@ int os_rename(const char_u *path, const char_u *new_path) int os_mkdir(const char *path, int32_t mode) FUNC_ATTR_NONNULL_ALL { - uv_fs_t request; - int result = uv_fs_mkdir(&fs_loop, &request, path, mode, NULL); - uv_fs_req_cleanup(&request); - return result; + int r; + RUN_UV_FS_FUNC(r, uv_fs_mkdir, path, mode, NULL); + return r; } /// Make a directory, with higher levels when needed @@ -555,10 +734,9 @@ int os_mkdtemp(const char *template, char *path) int os_rmdir(const char *path) FUNC_ATTR_NONNULL_ALL { - uv_fs_t request; - int result = uv_fs_rmdir(&fs_loop, &request, path, NULL); - uv_fs_req_cleanup(&request); - return result; + int r; + RUN_UV_FS_FUNC(r, uv_fs_rmdir, path, NULL); + return r; } /// Opens a directory. @@ -600,10 +778,9 @@ void os_closedir(Directory *dir) int os_remove(const char *path) FUNC_ATTR_NONNULL_ALL { - uv_fs_t request; - int result = uv_fs_unlink(&fs_loop, &request, path, NULL); - uv_fs_req_cleanup(&request); - return result; + int r; + RUN_UV_FS_FUNC(r, uv_fs_unlink, path, NULL); + return r; } /// Get the file information for a given path @@ -742,10 +919,222 @@ bool os_fileid_equal(const FileID *file_id_1, const FileID *file_id_2) /// @param file_info Pointer to a `FileInfo` /// @return `true` if the `FileID` and the `FileInfo` represent te same file. bool os_fileid_equal_fileinfo(const FileID *file_id, - const FileInfo *file_info) + const FileInfo *file_info) FUNC_ATTR_NONNULL_ALL { return file_id->inode == file_info->stat.st_ino && file_id->device_id == file_info->stat.st_dev; } +#ifdef WIN32 +# include <shlobj.h> + +/// When "fname" is the name of a shortcut (*.lnk) resolve the file it points +/// to and return that name in allocated memory. +/// Otherwise NULL is returned. +char_u * os_resolve_shortcut(char_u *fname) +{ + HRESULT hr; + IPersistFile *ppf = NULL; + OLECHAR wsz[MAX_PATH]; + char_u *rfname = NULL; + int len; + IShellLinkW *pslw = NULL; + WIN32_FIND_DATAW ffdw; + + // Check if the file name ends in ".lnk". Avoid calling CoCreateInstance(), + // it's quite slow. + if (fname == NULL) { + return rfname; + } + len = (int)STRLEN(fname); + if (len <= 4 || STRNICMP(fname + len - 4, ".lnk", 4) != 0) { + return rfname; + } + + CoInitialize(NULL); + + // create a link manager object and request its interface + hr = CoCreateInstance(&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, + &IID_IShellLinkW, (void **)&pslw); + if (hr == S_OK) { + WCHAR *p; + int conversion_result = utf8_to_utf16((char *)fname, &p); + if (conversion_result != 0) { + EMSG2("utf8_to_utf16 failed: %s", uv_strerror(conversion_result)); + } + + if (p != NULL) { + // Get a pointer to the IPersistFile interface. + hr = pslw->lpVtbl->QueryInterface( + pslw, &IID_IPersistFile, (void **)&ppf); + if (hr != S_OK) { + goto shortcut_errorw; + } + + // "load" the name and resolve the link + hr = ppf->lpVtbl->Load(ppf, p, STGM_READ); + if (hr != S_OK) { + goto shortcut_errorw; + } + +# if 0 // This makes Vim wait a long time if the target does not exist. + hr = pslw->lpVtbl->Resolve(pslw, NULL, SLR_NO_UI); + if (hr != S_OK) { + goto shortcut_errorw; + } +# endif + + // Get the path to the link target. + ZeroMemory(wsz, MAX_PATH * sizeof(WCHAR)); + hr = pslw->lpVtbl->GetPath(pslw, wsz, MAX_PATH, &ffdw, 0); + if (hr == S_OK && wsz[0] != NUL) { + int conversion_result = utf16_to_utf8(wsz, &rfname); + if (conversion_result != 0) { + EMSG2("utf16_to_utf8 failed: %s", uv_strerror(conversion_result)); + } + } + +shortcut_errorw: + xfree(p); + goto shortcut_end; + } + } + +shortcut_end: + // Release all interface pointers (both belong to the same object) + if (ppf != NULL) { + ppf->lpVtbl->Release(ppf); + } + if (pslw != NULL) { + pslw->lpVtbl->Release(pslw); + } + + CoUninitialize(); + return rfname; +} + +#endif + +int os_translate_sys_error(int sys_errno) { +#ifdef HAVE_UV_TRANSLATE_SYS_ERROR + return uv_translate_sys_error(sys_errno); +#elif WIN32 + // TODO(equalsraf): libuv does not yet expose uv_translate_sys_error() + // in its public API, include a version here until it can be used. + // See https://github.com/libuv/libuv/issues/79 +# ifndef ERROR_SYMLINK_NOT_SUPPORTED +# define ERROR_SYMLINK_NOT_SUPPORTED 1464 +# endif + + if (sys_errno <= 0) { + return sys_errno; // If < 0 then it's already a libuv error + } + + switch (sys_errno) { + case ERROR_NOACCESS: return UV_EACCES; + case WSAEACCES: return UV_EACCES; + case ERROR_ADDRESS_ALREADY_ASSOCIATED: return UV_EADDRINUSE; + case WSAEADDRINUSE: return UV_EADDRINUSE; + case WSAEADDRNOTAVAIL: return UV_EADDRNOTAVAIL; + case WSAEAFNOSUPPORT: return UV_EAFNOSUPPORT; + case WSAEWOULDBLOCK: return UV_EAGAIN; + case WSAEALREADY: return UV_EALREADY; + case ERROR_INVALID_FLAGS: return UV_EBADF; + case ERROR_INVALID_HANDLE: return UV_EBADF; + case ERROR_LOCK_VIOLATION: return UV_EBUSY; + case ERROR_PIPE_BUSY: return UV_EBUSY; + case ERROR_SHARING_VIOLATION: return UV_EBUSY; + case ERROR_OPERATION_ABORTED: return UV_ECANCELED; + case WSAEINTR: return UV_ECANCELED; + case ERROR_NO_UNICODE_TRANSLATION: return UV_ECHARSET; + case ERROR_CONNECTION_ABORTED: return UV_ECONNABORTED; + case WSAECONNABORTED: return UV_ECONNABORTED; + case ERROR_CONNECTION_REFUSED: return UV_ECONNREFUSED; + case WSAECONNREFUSED: return UV_ECONNREFUSED; + case ERROR_NETNAME_DELETED: return UV_ECONNRESET; + case WSAECONNRESET: return UV_ECONNRESET; + case ERROR_ALREADY_EXISTS: return UV_EEXIST; + case ERROR_FILE_EXISTS: return UV_EEXIST; + case ERROR_BUFFER_OVERFLOW: return UV_EFAULT; + case WSAEFAULT: return UV_EFAULT; + case ERROR_HOST_UNREACHABLE: return UV_EHOSTUNREACH; + case WSAEHOSTUNREACH: return UV_EHOSTUNREACH; + case ERROR_INSUFFICIENT_BUFFER: return UV_EINVAL; + case ERROR_INVALID_DATA: return UV_EINVAL; + case ERROR_INVALID_PARAMETER: return UV_EINVAL; + case ERROR_SYMLINK_NOT_SUPPORTED: return UV_EINVAL; + case WSAEINVAL: return UV_EINVAL; + case WSAEPFNOSUPPORT: return UV_EINVAL; + case WSAESOCKTNOSUPPORT: return UV_EINVAL; + case ERROR_BEGINNING_OF_MEDIA: return UV_EIO; + case ERROR_BUS_RESET: return UV_EIO; + case ERROR_CRC: return UV_EIO; + case ERROR_DEVICE_DOOR_OPEN: return UV_EIO; + case ERROR_DEVICE_REQUIRES_CLEANING: return UV_EIO; + case ERROR_DISK_CORRUPT: return UV_EIO; + case ERROR_EOM_OVERFLOW: return UV_EIO; + case ERROR_FILEMARK_DETECTED: return UV_EIO; + case ERROR_GEN_FAILURE: return UV_EIO; + case ERROR_INVALID_BLOCK_LENGTH: return UV_EIO; + case ERROR_IO_DEVICE: return UV_EIO; + case ERROR_NO_DATA_DETECTED: return UV_EIO; + case ERROR_NO_SIGNAL_SENT: return UV_EIO; + case ERROR_OPEN_FAILED: return UV_EIO; + case ERROR_SETMARK_DETECTED: return UV_EIO; + case ERROR_SIGNAL_REFUSED: return UV_EIO; + case WSAEISCONN: return UV_EISCONN; + case ERROR_CANT_RESOLVE_FILENAME: return UV_ELOOP; + case ERROR_TOO_MANY_OPEN_FILES: return UV_EMFILE; + case WSAEMFILE: return UV_EMFILE; + case WSAEMSGSIZE: return UV_EMSGSIZE; + case ERROR_FILENAME_EXCED_RANGE: return UV_ENAMETOOLONG; + case ERROR_NETWORK_UNREACHABLE: return UV_ENETUNREACH; + case WSAENETUNREACH: return UV_ENETUNREACH; + case WSAENOBUFS: return UV_ENOBUFS; + case ERROR_BAD_PATHNAME: return UV_ENOENT; + case ERROR_DIRECTORY: return UV_ENOENT; + case ERROR_FILE_NOT_FOUND: return UV_ENOENT; + case ERROR_INVALID_NAME: return UV_ENOENT; + case ERROR_INVALID_DRIVE: return UV_ENOENT; + case ERROR_INVALID_REPARSE_DATA: return UV_ENOENT; + case ERROR_MOD_NOT_FOUND: return UV_ENOENT; + case ERROR_PATH_NOT_FOUND: return UV_ENOENT; + case WSAHOST_NOT_FOUND: return UV_ENOENT; + case WSANO_DATA: return UV_ENOENT; + case ERROR_NOT_ENOUGH_MEMORY: return UV_ENOMEM; + case ERROR_OUTOFMEMORY: return UV_ENOMEM; + case ERROR_CANNOT_MAKE: return UV_ENOSPC; + case ERROR_DISK_FULL: return UV_ENOSPC; + case ERROR_EA_TABLE_FULL: return UV_ENOSPC; + case ERROR_END_OF_MEDIA: return UV_ENOSPC; + case ERROR_HANDLE_DISK_FULL: return UV_ENOSPC; + case ERROR_NOT_CONNECTED: return UV_ENOTCONN; + case WSAENOTCONN: return UV_ENOTCONN; + case ERROR_DIR_NOT_EMPTY: return UV_ENOTEMPTY; + case WSAENOTSOCK: return UV_ENOTSOCK; + case ERROR_NOT_SUPPORTED: return UV_ENOTSUP; + case ERROR_BROKEN_PIPE: return UV_EOF; + case ERROR_ACCESS_DENIED: return UV_EPERM; + case ERROR_PRIVILEGE_NOT_HELD: return UV_EPERM; + case ERROR_BAD_PIPE: return UV_EPIPE; + case ERROR_NO_DATA: return UV_EPIPE; + case ERROR_PIPE_NOT_CONNECTED: return UV_EPIPE; + case WSAESHUTDOWN: return UV_EPIPE; + case WSAEPROTONOSUPPORT: return UV_EPROTONOSUPPORT; + case ERROR_WRITE_PROTECT: return UV_EROFS; + case ERROR_SEM_TIMEOUT: return UV_ETIMEDOUT; + case WSAETIMEDOUT: return UV_ETIMEDOUT; + case ERROR_NOT_SAME_DEVICE: return UV_EXDEV; + case ERROR_INVALID_FUNCTION: return UV_EISDIR; + case ERROR_META_EXPANSION_TOO_LONG: return UV_E2BIG; + default: return UV_UNKNOWN; + } +#else + const int error = -errno; + STATIC_ASSERT(-EINTR == UV_EINTR, "Need to translate error codes"); + STATIC_ASSERT(-EAGAIN == UV_EAGAIN, "Need to translate error codes"); + STATIC_ASSERT(-ENOMEM == UV_ENOMEM, "Need to translate error codes"); + return error; +#endif +} diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index 7687b14f02..f264a26939 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -19,7 +19,7 @@ #include "nvim/getchar.h" #include "nvim/main.h" #include "nvim/misc1.h" -#include "nvim/misc2.h" +#include "nvim/state.h" #define READ_BUFFER_SIZE 0xfff #define INPUT_BUFFER_SIZE (READ_BUFFER_SIZE * 4) @@ -39,8 +39,6 @@ static int events_enabled = 0; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "os/input.c.generated.h" #endif -// Helper function used to push bytes from the 'event' key sequence partially -// between calls to os_inchar when maxlen < 3 void input_init(void) { @@ -60,8 +58,8 @@ void input_start(int fd) } global_fd = fd; - rstream_init_fd(&loop, &read_stream, fd, READ_BUFFER_SIZE, NULL); - rstream_start(&read_stream, read_cb); + rstream_init_fd(&main_loop, &read_stream, fd, READ_BUFFER_SIZE); + rstream_start(&read_stream, read_cb, NULL); } void input_stop(void) @@ -71,7 +69,7 @@ void input_stop(void) } rstream_stop(&read_stream); - stream_close(&read_stream, NULL); + stream_close(&read_stream, NULL, NULL); } static void cursorhold_event(void **argv) @@ -87,8 +85,8 @@ static void create_cursorhold_event(void) // have been called(inbuf_poll would return kInputAvail) // TODO(tarruda): Cursorhold should be implemented as a timer set during the // `state_check` callback for the states where it can be triggered. - assert(!events_enabled || queue_empty(loop.events)); - queue_put(loop.events, cursorhold_event, 0); + assert(!events_enabled || multiqueue_empty(main_loop.events)); + multiqueue_put(main_loop.events, cursorhold_event, 0); } // Low level input function @@ -147,7 +145,7 @@ bool os_char_avail(void) void os_breakcheck(void) { if (!got_int) { - loop_poll_events(&loop, 0); + loop_poll_events(&main_loop, 0); } } @@ -172,12 +170,13 @@ bool os_isatty(int fd) size_t input_enqueue(String keys) { - char *ptr = keys.data, *end = ptr + keys.size; + char *ptr = keys.data; + char *end = ptr + keys.size; while (rbuffer_space(input_buffer) >= 6 && ptr < end) { uint8_t buf[6] = { 0 }; - unsigned int new_size = trans_special((const uint8_t **)&ptr, keys.size, - buf, true); + unsigned int new_size + = trans_special((const uint8_t **)&ptr, (size_t)(end - ptr), buf, true); if (new_size) { new_size = handle_mouse_event(&ptr, buf, new_size); @@ -187,8 +186,7 @@ size_t input_enqueue(String keys) if (*ptr == '<') { char *old_ptr = ptr; - // Invalid or incomplete key sequence, skip until the next '>' or until - // *end + // Invalid or incomplete key sequence, skip until the next '>' or *end. do { ptr++; } while (ptr < end && *ptr != '>'); @@ -266,29 +264,32 @@ static unsigned int handle_mouse_event(char **ptr, uint8_t *buf, } static int orig_num_clicks = 0; - static int orig_mouse_code = 0; - static int orig_mouse_col = 0; - static int orig_mouse_row = 0; - static uint64_t orig_mouse_time = 0; // time of previous mouse click - uint64_t mouse_time = os_hrtime(); // time of current mouse click - - // compute the time elapsed since the previous mouse click and - // convert p_mouse from ms to ns - uint64_t timediff = mouse_time - orig_mouse_time; - uint64_t mouset = (uint64_t)p_mouset * 1000000; - if (mouse_code == orig_mouse_code - && timediff < mouset - && orig_num_clicks != 4 - && orig_mouse_col == mouse_col - && orig_mouse_row == mouse_row) { - orig_num_clicks++; - } else { - orig_num_clicks = 1; + if (mouse_code != KE_LEFTRELEASE && mouse_code != KE_RIGHTRELEASE + && mouse_code != KE_MIDDLERELEASE) { + static int orig_mouse_code = 0; + static int orig_mouse_col = 0; + static int orig_mouse_row = 0; + static uint64_t orig_mouse_time = 0; // time of previous mouse click + uint64_t mouse_time = os_hrtime(); // time of current mouse click (ns) + + // compute the time elapsed since the previous mouse click and + // convert p_mouse from ms to ns + uint64_t timediff = mouse_time - orig_mouse_time; + uint64_t mouset = (uint64_t)p_mouset * 1000000; + if (mouse_code == orig_mouse_code + && timediff < mouset + && orig_num_clicks != 4 + && orig_mouse_col == mouse_col + && orig_mouse_row == mouse_row) { + orig_num_clicks++; + } else { + orig_num_clicks = 1; + } + orig_mouse_code = mouse_code; + orig_mouse_col = mouse_col; + orig_mouse_row = mouse_row; + orig_mouse_time = mouse_time; } - orig_mouse_code = mouse_code; - orig_mouse_col = mouse_col; - orig_mouse_row = mouse_row; - orig_mouse_time = mouse_time; uint8_t modifiers = 0; if (orig_num_clicks == 2) { @@ -322,7 +323,7 @@ static bool input_poll(int ms) prof_inchar_enter(); } - LOOP_PROCESS_EVENTS_UNTIL(&loop, NULL, ms, input_ready() || input_eof); + LOOP_PROCESS_EVENTS_UNTIL(&main_loop, NULL, ms, input_ready() || input_eof); if (do_profiling == PROF_YES && ms) { prof_inchar_exit(); @@ -386,6 +387,8 @@ static void process_interrupts(void) } } +// 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) { static const uint8_t key[3] = { K_SPECIAL, KS_EXTRA, KE_EVENT }; @@ -419,5 +422,5 @@ static void read_error_exit(void) static bool pending_events(void) { - return events_enabled && !queue_empty(loop.events); + return events_enabled && !multiqueue_empty(main_loop.events); } diff --git a/src/nvim/os/os_defs.h b/src/nvim/os/os_defs.h index eee0cdd10b..14c210c69c 100644 --- a/src/nvim/os/os_defs.h +++ b/src/nvim/os/os_defs.h @@ -16,7 +16,7 @@ #define BASENAMELEN (NAME_MAX - 5) // Use the system path length if it makes sense. -#if defined(PATH_MAX) && (PATH_MAX > 1000) +#if defined(PATH_MAX) && (PATH_MAX > 1024) # define MAXPATHL PATH_MAX #else # define MAXPATHL 1024 @@ -46,4 +46,10 @@ /// negative libuv error codes are returned by a number of os functions. #define os_strerror uv_strerror +#ifdef WIN32 +# define os_strtok strtok_s +#else +# define os_strtok strtok_r +#endif + #endif // NVIM_OS_OS_DEFS_H diff --git a/src/nvim/os/pty_process.h b/src/nvim/os/pty_process.h new file mode 100644 index 0000000000..94923499ca --- /dev/null +++ b/src/nvim/os/pty_process.h @@ -0,0 +1,9 @@ +#ifndef NVIM_OS_PTY_PROCESS_H +#define NVIM_OS_PTY_PROCESS_H + +#ifdef WIN32 +# include "nvim/os/pty_process_win.h" +#else +# include "nvim/os/pty_process_unix.h" +#endif +#endif // NVIM_OS_PTY_PROCESS_H diff --git a/src/nvim/os/pty_process_unix.c b/src/nvim/os/pty_process_unix.c new file mode 100644 index 0000000000..b57a69b82b --- /dev/null +++ b/src/nvim/os/pty_process_unix.c @@ -0,0 +1,245 @@ +// Some of the code came from pangoterm and libuv +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> + +#include <termios.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/ioctl.h> + +// forkpty is not in POSIX, so headers are platform-specific +#if defined(__FreeBSD__) +# include <libutil.h> +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) +# include <util.h> +#else +# include <pty.h> +#endif + +#include <uv.h> + +#include "nvim/lib/klist.h" + +#include "nvim/event/loop.h" +#include "nvim/event/rstream.h" +#include "nvim/event/wstream.h" +#include "nvim/event/process.h" +#include "nvim/os/pty_process_unix.h" +#include "nvim/log.h" +#include "nvim/os/os.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/pty_process_unix.c.generated.h" +#endif + +bool pty_process_spawn(PtyProcess *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ + static struct termios termios; + if (!termios.c_cflag) { + init_termios(&termios); + } + + Process *proc = (Process *)ptyproc; + assert(!proc->err); + uv_signal_start(&proc->loop->children_watcher, chld_handler, SIGCHLD); + ptyproc->winsize = (struct winsize){ ptyproc->height, ptyproc->width, 0, 0 }; + uv_disable_stdio_inheritance(); + int master; + int pid = forkpty(&master, NULL, &termios, &ptyproc->winsize); + + if (pid < 0) { + ELOG("forkpty failed: %s", strerror(errno)); + return false; + } else if (pid == 0) { + init_child(ptyproc); + abort(); + } + + // make sure the master file descriptor is non blocking + int master_status_flags = fcntl(master, F_GETFL); + if (master_status_flags == -1) { + ELOG("Failed to get master descriptor status flags: %s", strerror(errno)); + goto error; + } + if (fcntl(master, F_SETFL, master_status_flags | O_NONBLOCK) == -1) { + ELOG("Failed to make master descriptor non-blocking: %s", strerror(errno)); + goto error; + } + + if (proc->in && !set_duplicating_descriptor(master, &proc->in->uv.pipe)) { + goto error; + } + if (proc->out && !set_duplicating_descriptor(master, &proc->out->uv.pipe)) { + goto error; + } + + ptyproc->tty_fd = master; + proc->pid = pid; + return true; + +error: + close(master); + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); + return false; +} + +void pty_process_resize(PtyProcess *ptyproc, uint16_t width, uint16_t height) + FUNC_ATTR_NONNULL_ALL +{ + ptyproc->winsize = (struct winsize){ height, width, 0, 0 }; + ioctl(ptyproc->tty_fd, TIOCSWINSZ, &ptyproc->winsize); +} + +void pty_process_close(PtyProcess *ptyproc) + FUNC_ATTR_NONNULL_ALL +{ + pty_process_close_master(ptyproc); + Process *proc = (Process *)ptyproc; + if (proc->internal_close_cb) { + proc->internal_close_cb(proc); + } +} + +void pty_process_close_master(PtyProcess *ptyproc) FUNC_ATTR_NONNULL_ALL +{ + if (ptyproc->tty_fd >= 0) { + close(ptyproc->tty_fd); + ptyproc->tty_fd = -1; + } +} + +void pty_process_teardown(Loop *loop) +{ + uv_signal_stop(&loop->children_watcher); +} + +static void init_child(PtyProcess *ptyproc) FUNC_ATTR_NONNULL_ALL +{ + unsetenv("COLUMNS"); + unsetenv("LINES"); + unsetenv("TERMCAP"); + unsetenv("COLORTERM"); + unsetenv("COLORFGBG"); + + signal(SIGCHLD, SIG_DFL); + signal(SIGHUP, SIG_DFL); + signal(SIGINT, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + signal(SIGTERM, SIG_DFL); + signal(SIGALRM, SIG_DFL); + + Process *proc = (Process *)ptyproc; + if (proc->cwd && os_chdir(proc->cwd) != 0) { + fprintf(stderr, "chdir failed: %s\n", strerror(errno)); + return; + } + + setenv("TERM", ptyproc->term_name ? ptyproc->term_name : "ansi", 1); + execvp(ptyproc->process.argv[0], ptyproc->process.argv); + fprintf(stderr, "execvp failed: %s\n", strerror(errno)); +} + +static void init_termios(struct termios *termios) FUNC_ATTR_NONNULL_ALL +{ + // Taken from pangoterm + termios->c_iflag = ICRNL|IXON; + termios->c_oflag = OPOST|ONLCR; +#ifdef TAB0 + termios->c_oflag |= TAB0; +#endif + termios->c_cflag = CS8|CREAD; + termios->c_lflag = ISIG|ICANON|IEXTEN|ECHO|ECHOE|ECHOK; + + cfsetspeed(termios, 38400); + +#ifdef IUTF8 + termios->c_iflag |= IUTF8; +#endif +#ifdef NL0 + termios->c_oflag |= NL0; +#endif +#ifdef CR0 + termios->c_oflag |= CR0; +#endif +#ifdef BS0 + termios->c_oflag |= BS0; +#endif +#ifdef VT0 + termios->c_oflag |= VT0; +#endif +#ifdef FF0 + termios->c_oflag |= FF0; +#endif +#ifdef ECHOCTL + termios->c_lflag |= ECHOCTL; +#endif +#ifdef ECHOKE + termios->c_lflag |= ECHOKE; +#endif + + termios->c_cc[VINTR] = 0x1f & 'C'; + termios->c_cc[VQUIT] = 0x1f & '\\'; + termios->c_cc[VERASE] = 0x7f; + termios->c_cc[VKILL] = 0x1f & 'U'; + termios->c_cc[VEOF] = 0x1f & 'D'; + termios->c_cc[VEOL] = _POSIX_VDISABLE; + termios->c_cc[VEOL2] = _POSIX_VDISABLE; + termios->c_cc[VSTART] = 0x1f & 'Q'; + termios->c_cc[VSTOP] = 0x1f & 'S'; + termios->c_cc[VSUSP] = 0x1f & 'Z'; + termios->c_cc[VREPRINT] = 0x1f & 'R'; + termios->c_cc[VWERASE] = 0x1f & 'W'; + termios->c_cc[VLNEXT] = 0x1f & 'V'; + termios->c_cc[VMIN] = 1; + termios->c_cc[VTIME] = 0; +} + +static bool set_duplicating_descriptor(int fd, uv_pipe_t *pipe) + FUNC_ATTR_NONNULL_ALL +{ + int fd_dup = dup(fd); + if (fd_dup < 0) { + ELOG("Failed to dup descriptor %d: %s", fd, strerror(errno)); + return false; + } + int uv_result = uv_pipe_open(pipe, fd_dup); + if (uv_result) { + ELOG("Failed to set pipe to descriptor %d: %s", + fd_dup, uv_strerror(uv_result)); + close(fd_dup); + return false; + } + return true; +} + +static void chld_handler(uv_signal_t *handle, int signum) +{ + int stat = 0; + int pid; + + do { + pid = waitpid(-1, &stat, WNOHANG); + } while (pid < 0 && errno == EINTR); + + if (pid <= 0) { + return; + } + + Loop *loop = handle->loop->data; + + kl_iter(WatcherPtr, loop->children, current) { + Process *proc = (*current)->data; + if (proc->pid == pid) { + if (WIFEXITED(stat)) { + proc->status = WEXITSTATUS(stat); + } else if (WIFSIGNALED(stat)) { + proc->status = WTERMSIG(stat); + } + proc->internal_exit_cb(proc); + break; + } + } +} diff --git a/src/nvim/os/pty_process_unix.h b/src/nvim/os/pty_process_unix.h new file mode 100644 index 0000000000..f7c57b3839 --- /dev/null +++ b/src/nvim/os/pty_process_unix.h @@ -0,0 +1,31 @@ +#ifndef NVIM_OS_PTY_PROCESS_UNIX_H +#define NVIM_OS_PTY_PROCESS_UNIX_H + +#include <sys/ioctl.h> + +#include "nvim/event/process.h" + +typedef struct pty_process { + Process process; + char *term_name; + uint16_t width, height; + struct winsize winsize; + int tty_fd; +} PtyProcess; + +static inline PtyProcess pty_process_init(Loop *loop, void *data) +{ + PtyProcess rv; + rv.process = process_init(loop, kProcessTypePty, data); + rv.term_name = NULL; + rv.width = 80; + rv.height = 24; + rv.tty_fd = -1; + return rv; +} + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/pty_process_unix.h.generated.h" +#endif + +#endif // NVIM_OS_PTY_PROCESS_UNIX_H diff --git a/src/nvim/os/pty_process_win.h b/src/nvim/os/pty_process_win.h new file mode 100644 index 0000000000..20cc589925 --- /dev/null +++ b/src/nvim/os/pty_process_win.h @@ -0,0 +1,28 @@ +#ifndef NVIM_OS_PTY_PROCESS_WIN_H +#define NVIM_OS_PTY_PROCESS_WIN_H + +#include "nvim/event/libuv_process.h" + +typedef struct pty_process { + Process process; + char *term_name; + uint16_t width, height; +} PtyProcess; + +#define pty_process_spawn(job) libuv_process_spawn((LibuvProcess *)job) +#define pty_process_close(job) libuv_process_close((LibuvProcess *)job) +#define pty_process_close_master(job) libuv_process_close((LibuvProcess *)job) +#define pty_process_resize(job, width, height) +#define pty_process_teardown(loop) + +static inline PtyProcess pty_process_init(Loop *loop, void *data) +{ + PtyProcess rv; + rv.process = process_init(loop, kProcessTypePty, data); + rv.term_name = NULL; + rv.width = 80; + rv.height = 24; + return rv; +} + +#endif // NVIM_OS_PTY_PROCESS_WIN_H diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c index f5a1637c94..9514936ad0 100644 --- a/src/nvim/os/shell.c +++ b/src/nvim/os/shell.c @@ -14,18 +14,20 @@ #include "nvim/os/shell.h" #include "nvim/os/signal.h" #include "nvim/types.h" +#include "nvim/main.h" #include "nvim/vim.h" #include "nvim/message.h" #include "nvim/memory.h" #include "nvim/ui.h" -#include "nvim/misc2.h" #include "nvim/screen.h" #include "nvim/memline.h" #include "nvim/option_defs.h" #include "nvim/charset.h" #include "nvim/strings.h" -#define DYNAMIC_BUFFER_INIT {NULL, 0, 0} +#define DYNAMIC_BUFFER_INIT { NULL, 0, 0 } +#define NS_1_SECOND 1000000000U // 1 second, in nanoseconds +#define OUT_DATA_THRESHOLD 1024 * 10U // 10KB, "a few screenfuls" of data. typedef struct { char *data; @@ -53,12 +55,12 @@ char **shell_build_argv(const char *cmd, const char *extra_args) size_t i = tokenize(p_sh, rv); if (extra_args) { - rv[i++] = xstrdup(extra_args); // Push a copy of `extra_args` + rv[i++] = xstrdup(extra_args); // Push a copy of `extra_args` } if (cmd) { - i += tokenize(p_shcf, rv + i); // Split 'shellcmdflag' - rv[i++] = xstrdup(cmd); // Push a copy of the command. + i += tokenize(p_shcf, rv + i); // Split 'shellcmdflag' + rv[i++] = shell_xescape_xquote(cmd); // Copy (and escape) `cmd`. } rv[i] = NULL; @@ -162,7 +164,7 @@ int os_call_shell(char_u *cmd, ShellOpts opts, char_u *extra_args) /// @param input The input to the shell (NULL for no input), passed to the /// stdin of the resulting process. /// @param len The length of the input buffer (not used if `input` == NULL) -/// @param[out] output A pointer to to a location where the output will be +/// @param[out] output Pointer to a location where the output will be /// allocated and stored. Will point to NULL if the shell /// command did not output anything. If NULL is passed, /// the shell output will be ignored. @@ -187,6 +189,9 @@ static int do_os_system(char **argv, bool silent, bool forward_output) { + out_data_decide_throttle(0); // Initialize throttle decider. + out_data_ring(NULL, 0); // Initialize output ring-buffer. + // the output buffer DynamicBuffer buf = DYNAMIC_BUFFER_INIT; stream_read_cb data_cb = system_data_cb; @@ -205,29 +210,29 @@ static int do_os_system(char **argv, xstrlcpy(prog, argv[0], MAXPATHL); Stream in, out, err; - LibuvProcess uvproc = libuv_process_init(&loop, &buf); + LibuvProcess uvproc = libuv_process_init(&main_loop, &buf); Process *proc = &uvproc.process; - Queue *events = queue_new_child(loop.events); + MultiQueue *events = multiqueue_new_child(main_loop.events); proc->events = events; proc->argv = argv; proc->in = input != NULL ? &in : NULL; proc->out = &out; proc->err = &err; if (!process_spawn(proc)) { - loop_poll_events(&loop, 0); - // Failed, probably due to `sh` not being executable + loop_poll_events(&main_loop, 0); + // Failed, probably due to 'sh' not being executable if (!silent) { MSG_PUTS(_("\nCannot execute ")); msg_outtrans((char_u *)prog); msg_putchar('\n'); } - queue_free(events); + multiqueue_free(events); return -1; } // We want to deal with stream events as fast a possible while queueing // process events, so reset everything to NULL. It prevents closing the - // streams while there's still data in the OS buffer(due to the process + // streams while there's still data in the OS buffer (due to the process // exiting before all data is read). if (input != NULL) { proc->in->events = NULL; @@ -235,10 +240,10 @@ static int do_os_system(char **argv, } proc->out->events = NULL; rstream_init(proc->out, 0); - rstream_start(proc->out, data_cb); + rstream_start(proc->out, data_cb, &buf); proc->err->events = NULL; rstream_init(proc->err, 0); - rstream_start(proc->err, data_cb); + rstream_start(proc->err, data_cb, &buf); // write the input, if any if (input) { @@ -250,14 +255,18 @@ static int do_os_system(char **argv, return -1; } // close the input stream after everything is written - wstream_set_write_cb(&in, shell_write_cb); + wstream_set_write_cb(&in, shell_write_cb, NULL); } - // invoke busy_start here so event_poll_until wont change the busy state for - // the UI + // Invoke busy_start here so LOOP_PROCESS_EVENTS_UNTIL will not change the + // busy state. ui_busy_start(); ui_flush(); int status = process_wait(proc, -1, NULL); + if (!got_int && out_data_decide_throttle(0)) { + // Last chunk of output was skipped; display it now. + out_data_ring(NULL, SIZE_MAX); + } ui_busy_stop(); // prepare the out parameters if requested @@ -277,8 +286,8 @@ static int do_os_system(char **argv, } } - assert(queue_empty(events)); - queue_free(events); + assert(multiqueue_empty(events)); + multiqueue_free(events); return status; } @@ -309,25 +318,192 @@ static void system_data_cb(Stream *stream, RBuffer *buf, size_t count, dbuf->len += nread; } +/// Tracks output received for the current executing shell command, and displays +/// a pulsing "..." when output should be skipped. Tracking depends on the +/// synchronous/blocking nature of ":!". +// +/// Purpose: +/// 1. CTRL-C is more responsive. #1234 #5396 +/// 2. Improves performance of :! (UI, esp. TUI, is the bottleneck). +/// 3. Avoids OOM during long-running, spammy :!. +/// +/// Vim does not need this hack because: +/// 1. :! in terminal-Vim runs in cooked mode, so CTRL-C is caught by the +/// terminal and raises SIGINT out-of-band. +/// 2. :! in terminal-Vim uses a tty (Nvim uses pipes), so commands +/// (e.g. `git grep`) may page themselves. +/// +/// @param size Length of data, used with internal state to decide whether +/// output should be skipped. size=0 resets the internal state and +/// returns the previous decision. +/// +/// @returns true if output should be skipped and pulse was displayed. +/// Returns the previous decision if size=0. +static bool out_data_decide_throttle(size_t size) +{ + static uint64_t started = 0; // Start time of the current throttle. + static size_t received = 0; // Bytes observed since last throttle. + static size_t visit = 0; // "Pulse" count of the current throttle. + static char pulse_msg[] = { ' ', ' ', ' ', '\0' }; + + if (!size) { + bool previous_decision = (visit > 0); + started = received = visit = 0; + return previous_decision; + } + + received += size; + if (received < OUT_DATA_THRESHOLD + // Display at least the first chunk of output even if it is big. + || (!started && received < size + 1000)) { + return false; + } else if (!visit) { + started = os_hrtime(); + } else if (visit % 20 == 0) { + uint64_t since = os_hrtime() - started; + if (since > (3 * NS_1_SECOND)) { + received = visit = 0; + return false; + } + } + + visit++; + // Pulse "..." at the bottom of the screen. + size_t tick = (visit % 20 == 0) + ? 3 // Force all dots "..." on last visit. + : (visit % 4); + pulse_msg[0] = (tick == 0) ? ' ' : '.'; + pulse_msg[1] = (tick == 0 || 1 == tick) ? ' ' : '.'; + pulse_msg[2] = (tick == 0 || 1 == tick || 2 == tick) ? ' ' : '.'; + if (visit == 1) { + screen_del_lines(0, 0, 1, (int)Rows, NULL); + } + int lastrow = (int)Rows - 1; + screen_puts_len((char_u *)pulse_msg, ARRAY_SIZE(pulse_msg), lastrow, 0, 0); + ui_flush(); + return true; +} + +/// Saves output in a quasi-ringbuffer. Used to ensure the last ~page of +/// output for a shell-command is always displayed. +/// +/// Init mode: Resets the internal state. +/// output = NULL +/// size = 0 +/// Print mode: Displays the current saved data. +/// output = NULL +/// size = SIZE_MAX +/// +/// @param output Data to save, or NULL to invoke a special mode. +/// @param size Length of `output`. +static void out_data_ring(char *output, size_t size) +{ +#define MAX_CHUNK_SIZE (OUT_DATA_THRESHOLD / 2) + static char last_skipped[MAX_CHUNK_SIZE]; // Saved output. + static size_t last_skipped_len = 0; + + assert(output != NULL || (size == 0 || size == SIZE_MAX)); + + if (output == NULL && size == 0) { // Init mode + last_skipped_len = 0; + return; + } + + if (output == NULL && size == SIZE_MAX) { // Print mode + out_data_append_to_screen(last_skipped, last_skipped_len, true); + return; + } + + // This is basically a ring-buffer... + if (size >= MAX_CHUNK_SIZE) { // Save mode + size_t start = size - MAX_CHUNK_SIZE; + memcpy(last_skipped, output + start, MAX_CHUNK_SIZE); + last_skipped_len = MAX_CHUNK_SIZE; + } else if (size > 0) { + // Length of the old data that can be kept. + size_t keep_len = MIN(last_skipped_len, MAX_CHUNK_SIZE - size); + size_t keep_start = last_skipped_len - keep_len; + // Shift the kept part of the old data to the start. + if (keep_start) { + memmove(last_skipped, last_skipped + keep_start, keep_len); + } + // Copy the entire new data to the remaining space. + memcpy(last_skipped + keep_len, output, size); + last_skipped_len = keep_len + size; + } +} + +/// Continue to append data to last screen line. +/// +/// @param output Data to append to screen lines. +/// @param remaining Size of data. +/// @param new_line If true, next data output will be on a new line. +static void out_data_append_to_screen(char *output, size_t remaining, + bool new_line) +{ + static colnr_T last_col = 0; // Column of last row to append to. + + size_t off = 0; + int last_row = (int)Rows - 1; + + while (off < remaining) { + // Found end of line? + if (output[off] == NL) { + // Can we start a new line or do we need to continue the last one? + if (last_col == 0) { + screen_del_lines(0, 0, 1, (int)Rows, NULL); + } + screen_puts_len((char_u *)output, (int)off, last_row, last_col, 0); + last_col = 0; + + size_t skip = off + 1; + output += skip; + remaining -= skip; + off = 0; + continue; + } + + // Translate NUL to SOH + if (output[off] == NUL) { + output[off] = 1; + } + + off++; + } + + if (remaining) { + if (last_col == 0) { + screen_del_lines(0, 0, 1, (int)Rows, NULL); + } + screen_puts_len((char_u *)output, (int)remaining, last_row, last_col, 0); + last_col += (colnr_T)remaining; + } + + if (new_line) { + last_col = 0; + } + + ui_flush(); +} + static void out_data_cb(Stream *stream, RBuffer *buf, size_t count, void *data, bool eof) { + // We always output the whole buffer, so the buffer can never + // wrap around. size_t cnt; char *ptr = rbuffer_read_ptr(buf, &cnt); - if (!cnt) { - return; + if (ptr != NULL && cnt > 0 + && out_data_decide_throttle(cnt)) { // Skip output above a threshold. + // Save the skipped output. If it is the final chunk, we display it later. + out_data_ring(ptr, cnt); + } else { + out_data_append_to_screen(ptr, cnt, eof); } - size_t written = write_output(ptr, cnt, false, eof); - // No output written, force emptying the Rbuffer if it is full. - if (!written && rbuffer_size(buf) == rbuffer_capacity(buf)) { - screen_del_lines(0, 0, 1, (int)Rows, NULL); - screen_puts_len((char_u *)ptr, (int)cnt, (int)Rows - 1, 0, 0); - written = cnt; - } - if (written) { - rbuffer_consumed(buf, written); + if (cnt) { + rbuffer_consumed(buf, cnt); } } @@ -500,5 +676,51 @@ static size_t write_output(char *output, size_t remaining, bool to_buffer, static void shell_write_cb(Stream *stream, void *data, int status) { - stream_close(stream, NULL); + if (status) { + // Can happen if system() tries to send input to a shell command that was + // backgrounded (:call system("cat - &", "foo")). #3529 #5241 + msg_schedule_emsgf(_("E5677: Error writing input to shell-command: %s"), + uv_err_name(status)); + } + if (stream->closed) { // Process may have exited before this write. + ELOG("stream was already closed"); + return; + } + stream_close(stream, NULL, NULL); +} + +/// Applies 'shellxescape' (p_sxe) and 'shellxquote' (p_sxq) to a command. +/// +/// @param cmd Command string +/// @return Escaped/quoted command string (allocated). +static char *shell_xescape_xquote(const char *cmd) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_MALLOC FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (*p_sxq == NUL) { + return xstrdup(cmd); + } + + const char *ecmd = cmd; + if (*p_sxe != NUL && STRCMP(p_sxq, "(") == 0) { + ecmd = (char *)vim_strsave_escaped_ext((char_u *)cmd, p_sxe, '^', false); + } + size_t ncmd_size = strlen(ecmd) + STRLEN(p_sxq) * 2 + 1; + char *ncmd = xmalloc(ncmd_size); + + // When 'shellxquote' is ( append ). + // When 'shellxquote' is "( append )". + if (STRCMP(p_sxq, "(") == 0) { + vim_snprintf(ncmd, ncmd_size, "(%s)", ecmd); + } else if (STRCMP(p_sxq, "\"(") == 0) { + vim_snprintf(ncmd, ncmd_size, "\"(%s)\"", ecmd); + } else { + vim_snprintf(ncmd, ncmd_size, "%s%s%s", p_sxq, ecmd, p_sxq); + } + + if (ecmd != cmd) { + xfree((void *)ecmd); + } + + return ncmd; } + diff --git a/src/nvim/os/signal.c b/src/nvim/os/signal.c index 0ff6016e32..1ac6d3f5e1 100644 --- a/src/nvim/os/signal.c +++ b/src/nvim/os/signal.c @@ -2,15 +2,18 @@ #include <stdbool.h> #include <uv.h> +#ifndef WIN32 +# include <signal.h> // for sigset_t +#endif #include "nvim/ascii.h" #include "nvim/vim.h" #include "nvim/globals.h" #include "nvim/memline.h" #include "nvim/eval.h" +#include "nvim/main.h" #include "nvim/memory.h" #include "nvim/misc1.h" -#include "nvim/misc2.h" #include "nvim/event/signal.h" #include "nvim/os/signal.h" #include "nvim/event/loop.h" @@ -28,10 +31,20 @@ static bool rejecting_deadly; void signal_init(void) { - signal_watcher_init(&loop, &spipe, NULL); - signal_watcher_init(&loop, &shup, NULL); - signal_watcher_init(&loop, &squit, NULL); - signal_watcher_init(&loop, &sterm, NULL); +#ifndef WIN32 + // Ensure a clean slate by unblocking all signals. For example, if SIGCHLD is + // blocked, libuv may hang after spawning a subprocess on Linux. #5230 + sigset_t mask; + sigemptyset(&mask); + if (pthread_sigmask(SIG_SETMASK, &mask, NULL) != 0) { + ELOG("Could not unblock signals, nvim might behave strangely."); + } +#endif + + signal_watcher_init(&main_loop, &spipe, NULL); + signal_watcher_init(&main_loop, &shup, NULL); + signal_watcher_init(&main_loop, &squit, NULL); + signal_watcher_init(&main_loop, &sterm, NULL); #ifdef SIGPIPE signal_watcher_start(&spipe, on_signal, SIGPIPE); #endif @@ -41,7 +54,7 @@ void signal_init(void) #endif signal_watcher_start(&sterm, on_signal, SIGTERM); #ifdef SIGPWR - signal_watcher_init(&loop, &spwr, NULL); + signal_watcher_init(&main_loop, &spwr, NULL); signal_watcher_start(&spwr, on_signal, SIGPWR); #endif } diff --git a/src/nvim/os/stdpaths.c b/src/nvim/os/stdpaths.c index 81ceb919c4..afb9bdec31 100644 --- a/src/nvim/os/stdpaths.c +++ b/src/nvim/os/stdpaths.c @@ -16,20 +16,29 @@ static const char *xdg_env_vars[] = { [kXDGDataDirs] = "XDG_DATA_DIRS", }; +#ifdef WIN32 +static const char *const xdg_defaults_env_vars[] = { + [kXDGConfigHome] = "LOCALAPPDATA", + [kXDGDataHome] = "LOCALAPPDATA", + [kXDGCacheHome] = "TEMP", + [kXDGRuntimeDir] = NULL, + [kXDGConfigDirs] = NULL, + [kXDGDataDirs] = NULL, +}; +#endif + /// Defaults for XDGVarType values /// /// Used in case environment variables contain nothing. Need to be expanded. static const char *const xdg_defaults[] = { #ifdef WIN32 - // Windows - [kXDGConfigHome] = "$LOCALAPPDATA", - [kXDGDataHome] = "$LOCALAPPDATA", - [kXDGCacheHome] = "$TEMP", + [kXDGConfigHome] = "~\\AppData\\Local", + [kXDGDataHome] = "~\\AppData\\Local", + [kXDGCacheHome] = "~\\AppData\\Local\\Temp", [kXDGRuntimeDir] = NULL, [kXDGConfigDirs] = NULL, [kXDGDataDirs] = NULL, #else - // Linux, BSD, CYGWIN, Apple [kXDGConfigHome] = "~/.config", [kXDGDataHome] = "~/.local/share", [kXDGCacheHome] = "~/.cache", @@ -50,7 +59,14 @@ char *stdpaths_get_xdg_var(const XDGVarType idx) const char *const env = xdg_env_vars[idx]; const char *const fallback = xdg_defaults[idx]; - const char *const env_val = os_getenv(env); + const char *env_val = os_getenv(env); + +#ifdef WIN32 + if (env_val == NULL) { + env_val = os_getenv(xdg_defaults_env_vars[idx]); + } +#endif + char *ret = NULL; if (env_val != NULL) { ret = xstrdup(env_val); @@ -100,18 +116,30 @@ char *stdpaths_user_conf_subpath(const char *fname) /// /// @param[in] fname New component of the path. /// @param[in] trailing_pathseps Amount of trailing path separators to add. +/// @param[in] escape_commas If true, all commas will be escaped. /// -/// @return [allocated] `$XDG_DATA_HOME/nvim/{fname}` +/// @return [allocated] `$XDG_DATA_HOME/nvim/{fname}`. char *stdpaths_user_data_subpath(const char *fname, - const size_t trailing_pathseps) + const size_t trailing_pathseps, + const bool escape_commas) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET { char *ret = concat_fnames_realloc(get_xdg_home(kXDGDataHome), fname, true); - if (trailing_pathseps) { - const size_t len = strlen(ret); - ret = xrealloc(ret, len + trailing_pathseps + 1); - memset(ret + len, PATHSEP, trailing_pathseps); - ret[len + trailing_pathseps] = NUL; + const size_t len = strlen(ret); + const size_t numcommas = (escape_commas ? memcnt(ret, ',', len) : 0); + if (numcommas || trailing_pathseps) { + ret = xrealloc(ret, len + trailing_pathseps + numcommas + 1); + for (size_t i = 0 ; i < len + numcommas ; i++) { + if (ret[i] == ',') { + memmove(ret + i + 1, ret + i, len - i + numcommas); + ret[i] = '\\'; + i++; + } + } + if (trailing_pathseps) { + memset(ret + len + numcommas, PATHSEP, trailing_pathseps); + } + ret[len + trailing_pathseps + numcommas] = NUL; } return ret; } diff --git a/src/nvim/os/time.c b/src/nvim/os/time.c index 188f0802c9..8ce2ecf4f4 100644 --- a/src/nvim/os/time.c +++ b/src/nvim/os/time.c @@ -7,8 +7,10 @@ #include <uv.h> #include "nvim/os/time.h" +#include "nvim/os/input.h" #include "nvim/event/loop.h" #include "nvim/vim.h" +#include "nvim/main.h" static uv_mutex_t delay_mutex; static uv_cond_t delay_cond; @@ -33,38 +35,54 @@ uint64_t os_hrtime(void) return uv_hrtime(); } -/// Sleeps for a certain amount of milliseconds +/// Sleeps for a certain amount of milliseconds. /// /// @param milliseconds Number of milliseconds to sleep -/// @param ignoreinput If true, allow a SIGINT to interrupt us +/// @param ignoreinput If true, only SIGINT (CTRL-C) can interrupt. void os_delay(uint64_t milliseconds, bool ignoreinput) { if (ignoreinput) { if (milliseconds > INT_MAX) { milliseconds = INT_MAX; } - LOOP_PROCESS_EVENTS_UNTIL(&loop, NULL, (int)milliseconds, got_int); + LOOP_PROCESS_EVENTS_UNTIL(&main_loop, NULL, (int)milliseconds, got_int); } else { - os_microdelay(milliseconds * 1000); + os_microdelay(milliseconds * 1000u, ignoreinput); } } -/// Sleeps for a certain amount of microseconds +/// Sleeps for a certain amount of microseconds. /// -/// @param microseconds Number of microseconds to sleep -void os_microdelay(uint64_t microseconds) +/// @param ms Number of microseconds to sleep. +/// @param ignoreinput If true, ignore all input (including SIGINT/CTRL-C). +/// If false, waiting is aborted on any input. +void os_microdelay(uint64_t ms, bool ignoreinput) { - uint64_t elapsed = 0; - uint64_t ns = microseconds * 1000; // convert to nanoseconds + uint64_t elapsed = 0u; uint64_t base = uv_hrtime(); + // Convert microseconds to nanoseconds, or UINT64_MAX on overflow. + const uint64_t ns = (ms < UINT64_MAX / 1000u) ? ms * 1000u : UINT64_MAX; uv_mutex_lock(&delay_mutex); while (elapsed < ns) { - if (uv_cond_timedwait(&delay_cond, &delay_mutex, ns - elapsed) - == UV_ETIMEDOUT) + // If ignoring input, we simply wait the full delay. + // Else we check for input in ~100ms intervals. + const uint64_t ns_delta = ignoreinput + ? ns - elapsed + : MIN(ns - elapsed, 100000000u); // 100ms + + const int rv = uv_cond_timedwait(&delay_cond, &delay_mutex, ns_delta); + if (0 != rv && UV_ETIMEDOUT != rv) { + assert(false); + break; + } // Else: Timeout proceeded normally. + + if (!ignoreinput && os_char_avail()) { break; - uint64_t now = uv_hrtime(); + } + + const uint64_t now = uv_hrtime(); elapsed += now - base; base = now; } diff --git a/src/nvim/os/users.c b/src/nvim/os/users.c index 8ebb7562ef..1c94ef0067 100644 --- a/src/nvim/os/users.c +++ b/src/nvim/os/users.c @@ -6,7 +6,6 @@ #include "nvim/os/os.h" #include "nvim/garray.h" #include "nvim/memory.h" -#include "nvim/misc2.h" #include "nvim/strings.h" #ifdef HAVE_PWD_H # include <pwd.h> |