mirror of
https://github.com/lihop/godot-xterm.git
synced 2024-11-25 02:30:27 +01:00
chore(tidyup): remove old unused code
This code was left around for reference during development, but is no longer used or needed.
This commit is contained in:
parent
c4f0afc4f2
commit
d7c83b3fe9
20 changed files with 0 additions and 4588 deletions
|
@ -1,200 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021-2023 Leroy Hopson <godot-xterm@leroy.geek.nz>
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
#include "libuv_utils.h"
|
|
||||||
#include <godot_cpp/classes/global_constants.hpp>
|
|
||||||
#include <uv.h>
|
|
||||||
|
|
||||||
using namespace godot;
|
|
||||||
|
|
||||||
void LibuvUtils::_bind_methods() {
|
|
||||||
ClassDB::bind_static_method("LibuvUtils", D_METHOD("get_os_environ"),
|
|
||||||
&LibuvUtils::get_os_environ);
|
|
||||||
ClassDB::bind_static_method("LibuvUtils", D_METHOD("get_os_release"),
|
|
||||||
&LibuvUtils::get_os_release);
|
|
||||||
ClassDB::bind_static_method("LibuvUtils", D_METHOD("get_cwd"),
|
|
||||||
&LibuvUtils::get_cwd);
|
|
||||||
|
|
||||||
ClassDB::bind_static_method("LibuvUtils", D_METHOD("kill", "pid", "signum"),
|
|
||||||
&LibuvUtils::kill);
|
|
||||||
}
|
|
||||||
|
|
||||||
LibuvUtils::LibuvUtils() {}
|
|
||||||
LibuvUtils::~LibuvUtils() {}
|
|
||||||
|
|
||||||
Dictionary LibuvUtils::get_os_environ() {
|
|
||||||
Dictionary result;
|
|
||||||
|
|
||||||
uv_env_item_t *env;
|
|
||||||
int count;
|
|
||||||
uv_os_environ(&env, &count);
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
result[String(env[i].name)] = String(env[i].value);
|
|
||||||
}
|
|
||||||
|
|
||||||
uv_os_free_environ(env, count);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
String LibuvUtils::get_os_release() { return "TODO"; }
|
|
||||||
|
|
||||||
String LibuvUtils::get_cwd() {
|
|
||||||
#ifndef PATH_MAX
|
|
||||||
#define PATH_MAX MAX_PATH
|
|
||||||
#endif
|
|
||||||
size_t size = PATH_MAX;
|
|
||||||
char *buffer = (char *)malloc(size * sizeof(char));
|
|
||||||
int err;
|
|
||||||
|
|
||||||
err = uv_cwd(buffer, &size);
|
|
||||||
|
|
||||||
if (err == UV_ENOBUFS) {
|
|
||||||
// Buffer was too small. `size` has been set to the required length, so
|
|
||||||
// resize buffer and try again.
|
|
||||||
buffer = (char *)realloc(buffer, size * sizeof(char));
|
|
||||||
err = uv_cwd(buffer, &size);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err < 0) {
|
|
||||||
UV_ERR_PRINT(err);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
String result = String(buffer);
|
|
||||||
std::free(buffer);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Error LibuvUtils::kill(int pid, int signum) {
|
|
||||||
RETURN_UV_ERR(uv_kill(pid, signum));
|
|
||||||
}
|
|
||||||
|
|
||||||
Error LibuvUtils::translate_uv_errno(int uv_err) {
|
|
||||||
if (uv_err >= 0)
|
|
||||||
return OK;
|
|
||||||
|
|
||||||
// Rough translation of libuv error to godot error.
|
|
||||||
// Not necessarily accurate.
|
|
||||||
|
|
||||||
switch (uv_err) {
|
|
||||||
case UV_EEXIST: // file already exists
|
|
||||||
return ERR_ALREADY_EXISTS;
|
|
||||||
|
|
||||||
case UV_EADDRINUSE: // address already in use
|
|
||||||
return ERR_ALREADY_IN_USE;
|
|
||||||
|
|
||||||
case UV_EBUSY: // resource busy or locked
|
|
||||||
case UV_ETXTBSY: // text file is busy
|
|
||||||
return ERR_BUSY;
|
|
||||||
|
|
||||||
case UV_ECONNREFUSED: // connection refused
|
|
||||||
return ERR_CANT_CONNECT;
|
|
||||||
|
|
||||||
case UV_ECONNABORTED: // software caused connection abort
|
|
||||||
case UV_ECONNRESET: // connection reset by peer
|
|
||||||
case UV_EISCONN: // socket is already connected
|
|
||||||
case UV_ENOTCONN: // socket is not connected
|
|
||||||
return ERR_CONNECTION_ERROR;
|
|
||||||
|
|
||||||
case UV_ENODEV: // no such device
|
|
||||||
case UV_ENXIO: // no such device or address
|
|
||||||
case UV_ESRCH: // no such process
|
|
||||||
return ERR_DOES_NOT_EXIST;
|
|
||||||
|
|
||||||
case UV_EROFS: // read-only file system
|
|
||||||
return ERR_FILE_CANT_WRITE;
|
|
||||||
|
|
||||||
case UV_EOF: // end of file
|
|
||||||
return ERR_FILE_EOF;
|
|
||||||
|
|
||||||
case UV_ENOENT: // no such file or directory
|
|
||||||
return ERR_FILE_NOT_FOUND;
|
|
||||||
|
|
||||||
case UV_EAI_BADFLAGS: // bad ai_flags value
|
|
||||||
case UV_EAI_BADHINTS: // invalid value for hints
|
|
||||||
case UV_EFAULT: // bad address in system call argument
|
|
||||||
case UV_EFTYPE: // inappropriate file type or format
|
|
||||||
case UV_EINVAL: // invalid argument
|
|
||||||
case UV_ENOTTY: // inappropriate ioctl for device
|
|
||||||
case UV_EPROTOTYPE: // protocol wrong type for socket
|
|
||||||
return ERR_INVALID_PARAMETER; // Parameter passed is invalid
|
|
||||||
|
|
||||||
case UV_ENOSYS: // function not implemented
|
|
||||||
return ERR_METHOD_NOT_FOUND;
|
|
||||||
|
|
||||||
case UV_EAI_MEMORY: // out of memory
|
|
||||||
return ERR_OUT_OF_MEMORY;
|
|
||||||
|
|
||||||
case UV_E2BIG: // argument list too long
|
|
||||||
case UV_EFBIG: // file too large
|
|
||||||
case UV_EMSGSIZE: // message too long
|
|
||||||
case UV_ENAMETOOLONG: // name too long
|
|
||||||
case UV_EOVERFLOW: // value too large for defined data type
|
|
||||||
case UV_ERANGE: // result too large
|
|
||||||
return ERR_PARAMETER_RANGE_ERROR; // Parameter given out of range
|
|
||||||
|
|
||||||
case UV_ETIMEDOUT:
|
|
||||||
return ERR_TIMEOUT; // connection timed out
|
|
||||||
|
|
||||||
case UV_EACCES: // permission denied
|
|
||||||
case UV_EPERM: // operation not permitted
|
|
||||||
case UV_EXDEV: // cross-device link not permitted
|
|
||||||
return ERR_UNAUTHORIZED;
|
|
||||||
|
|
||||||
case UV_EADDRNOTAVAIL: // address not available
|
|
||||||
case UV_EAFNOSUPPORT: // address family not supported
|
|
||||||
case UV_EAGAIN: // resource temporarily unavailable
|
|
||||||
case UV_EAI_ADDRFAMILY: // address family not supported
|
|
||||||
case UV_EAI_FAMILY: // ai_family not supported
|
|
||||||
case UV_EAI_SERVICE: // service not available for socket type
|
|
||||||
case UV_EAI_SOCKTYPE: // socket type not supported
|
|
||||||
case UV_ENOPROTOOPT: // protocol not available
|
|
||||||
case UV_ENOTSUP: // operation not supported on socket
|
|
||||||
case UV_EPROTONOSUPPORT: // protocol not supported
|
|
||||||
case UV_ESOCKTNOSUPPORT: // socket type not supported
|
|
||||||
return ERR_UNAVAILABLE; // What is requested is
|
|
||||||
// unsupported/unavailable
|
|
||||||
|
|
||||||
case UV_EAI_NODATA: // no address
|
|
||||||
case UV_EDESTADDRREQ: // destination address required
|
|
||||||
return ERR_UNCONFIGURED;
|
|
||||||
|
|
||||||
case UV_EAI_AGAIN: // temporary failure
|
|
||||||
case UV_EAI_CANCELED: // request canceled
|
|
||||||
case UV_EAI_FAIL: // permanent failure
|
|
||||||
case UV_EAI_NONAME: // unknown node or service
|
|
||||||
case UV_EAI_OVERFLOW: // argument buffer overflow
|
|
||||||
case UV_EAI_PROTOCOL: // resolved protocol is unknown
|
|
||||||
case UV_EALREADY: // connection already in progress
|
|
||||||
case UV_EBADF: // bad file descriptor
|
|
||||||
case UV_ECANCELED: // operation canceled
|
|
||||||
case UV_ECHARSET: // invalid Unicode character
|
|
||||||
case UV_EHOSTUNREACH: // host is unreachable
|
|
||||||
case UV_EIO: // i/o error
|
|
||||||
case UV_EILSEQ: // illegal byte sequence
|
|
||||||
case UV_EISDIR: // illegal operation on a directory
|
|
||||||
case UV_ELOOP: // too many symbolic links encountered
|
|
||||||
case UV_EMFILE: // too many open files
|
|
||||||
case UV_ENETDOWN: // network is down
|
|
||||||
case UV_ENETUNREACH: // network is unreachable
|
|
||||||
case UV_ENFILE: // file table overflow
|
|
||||||
case UV_ENOBUFS: // no buffer space available
|
|
||||||
case UV_ENOMEM: // not enough memory
|
|
||||||
case UV_ESHUTDOWN: // cannot send after transport endpoint shutdown
|
|
||||||
case UV_EINTR: // interrupted system call
|
|
||||||
case UV_EMLINK: // too many links
|
|
||||||
case UV_ENONET: // machine is not on the network
|
|
||||||
case UV_ENOSPC: // no space left on device
|
|
||||||
case UV_ENOTDIR: // not a directory
|
|
||||||
case UV_ENOTEMPTY: // directory not empty
|
|
||||||
case UV_ENOTSOCK: // socket operation on non-socket
|
|
||||||
case UV_EPIPE: // broken pipe
|
|
||||||
case UV_EPROTO: // protocol error
|
|
||||||
case UV_ESPIPE: // invalid seek
|
|
||||||
case UV_UNKNOWN: // unknown error
|
|
||||||
default:
|
|
||||||
return FAILED; // Generic fail error
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
#ifndef GODOT_XTERM_UV_UTILS_H
|
|
||||||
#define GODOT_XTERM_UV_UTILS_H
|
|
||||||
|
|
||||||
#include <godot_cpp/classes/ref_counted.hpp>
|
|
||||||
#include <uv.h>
|
|
||||||
|
|
||||||
#define UV_ERR_PRINT(uv_err) \
|
|
||||||
ERR_PRINT(String(uv_err_name(uv_err)) + String(": ") + \
|
|
||||||
String(uv_strerror(uv_err)));
|
|
||||||
|
|
||||||
#define RETURN_UV_ERR(uv_err) \
|
|
||||||
if (uv_err < 0) { \
|
|
||||||
UV_ERR_PRINT(uv_err); \
|
|
||||||
} \
|
|
||||||
return LibuvUtils::translate_uv_errno(uv_err);
|
|
||||||
|
|
||||||
#define RETURN_IF_UV_ERR(uv_err) \
|
|
||||||
if (uv_err < 0) { \
|
|
||||||
RETURN_UV_ERR(uv_err); \
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace godot {
|
|
||||||
|
|
||||||
class LibuvUtils : public RefCounted {
|
|
||||||
GDCLASS(LibuvUtils, RefCounted)
|
|
||||||
|
|
||||||
public:
|
|
||||||
LibuvUtils();
|
|
||||||
~LibuvUtils();
|
|
||||||
|
|
||||||
static Dictionary get_os_environ();
|
|
||||||
static String get_os_release();
|
|
||||||
static String get_cwd();
|
|
||||||
|
|
||||||
static Error kill(int pid, int signum);
|
|
||||||
|
|
||||||
public:
|
|
||||||
static Error translate_uv_errno(int uv_err);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
static void _bind_methods();
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace godot
|
|
||||||
|
|
||||||
#endif // GODOT_XTERM_UV_UTILS_H
|
|
|
@ -1,6 +0,0 @@
|
||||||
The code under the `node_pty` directory is taken from the [node-pty project](https://github.com/microsoft/node-pty), which in turn contains code from the [Tmux project](http://tmux.sourceforge.net/).
|
|
||||||
|
|
||||||
The code has been modified to remove references to node/V8 and make it compatible with GDNative. Any copyrightable modifications are released under the [same license](/addons/godot_xterm/LICENSE) as the rest of the GodotXterm project.
|
|
||||||
|
|
||||||
The text of the node-pty license can be found in [LICENSE_node-pty](./LICENSE_node-pty)
|
|
||||||
The text of the tmux license can be found in [LICENSE_tmux](./LICENSE_tmux)
|
|
|
@ -1,69 +0,0 @@
|
||||||
Copyright (c) 2012-2015, Christopher Jeffrey (https://github.com/chjj/)
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2016, Daniel Imms (http://www.growingwiththeweb.com)
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2018 - present Microsoft Corporation
|
|
||||||
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,15 +0,0 @@
|
||||||
Copyright (c) 2009 Nicholas Marriott <nicm@users.sourceforge.net>
|
|
||||||
Copyright (c) 2009 Joshua Elsasser <josh@elsasser.org>
|
|
||||||
Copyright (c) 2009 Todd Carson <toc@daybefore.net>
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and distribute this software for any
|
|
||||||
purpose with or without fee is hereby granted, provided that the above
|
|
||||||
copyright notice and this permission notice appear in all copies.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
|
||||||
OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
@ -1,433 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
|
|
||||||
* Copyright (c) 2016, Daniel Imms (MIT License).
|
|
||||||
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
|
||||||
* Copyright (c) 2021, Leroy Hopson (MIT License).
|
|
||||||
*
|
|
||||||
* pty.cc:
|
|
||||||
* This file is responsible for starting processes
|
|
||||||
* with pseudo-terminal file descriptors.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// node versions lower than 10 define this as 0x502 which disables many of the
|
|
||||||
// definitions needed to compile
|
|
||||||
#include <node_version.h>
|
|
||||||
#if NODE_MODULE_VERSION <= 57
|
|
||||||
#define _WIN32_WINNT 0x600
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "path_util.h"
|
|
||||||
#include <Shlwapi.h> // PathCombine, PathIsRelative
|
|
||||||
#include <Windows.h>
|
|
||||||
#include <iostream>
|
|
||||||
#include <nan.h>
|
|
||||||
#include <sstream>
|
|
||||||
#include <string>
|
|
||||||
#include <strsafe.h>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
extern "C" void init(v8::Local<v8::Object>);
|
|
||||||
|
|
||||||
// Taken from the RS5 Windows SDK, but redefined here in case we're targeting <=
|
|
||||||
// 17134
|
|
||||||
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
|
|
||||||
#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \
|
|
||||||
ProcThreadAttributeValue(22, FALSE, TRUE, FALSE)
|
|
||||||
|
|
||||||
typedef VOID *HPCON;
|
|
||||||
typedef HRESULT(__stdcall *PFNCREATEPSEUDOCONSOLE)(COORD c, HANDLE hIn,
|
|
||||||
HANDLE hOut, DWORD dwFlags,
|
|
||||||
HPCON *phpcon);
|
|
||||||
typedef HRESULT(__stdcall *PFNRESIZEPSEUDOCONSOLE)(HPCON hpc, COORD newSize);
|
|
||||||
typedef void(__stdcall *PFNCLOSEPSEUDOCONSOLE)(HPCON hpc);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct pty_baton {
|
|
||||||
int id;
|
|
||||||
HANDLE hIn;
|
|
||||||
HANDLE hOut;
|
|
||||||
HPCON hpc;
|
|
||||||
|
|
||||||
HANDLE hShell;
|
|
||||||
HANDLE hWait;
|
|
||||||
Nan::Callback cb;
|
|
||||||
uv_async_t async;
|
|
||||||
uv_thread_t tid;
|
|
||||||
|
|
||||||
pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc)
|
|
||||||
: id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc){};
|
|
||||||
};
|
|
||||||
|
|
||||||
static std::vector<pty_baton *> ptyHandles;
|
|
||||||
static volatile LONG ptyCounter;
|
|
||||||
|
|
||||||
static pty_baton *get_pty_baton(int id) {
|
|
||||||
for (size_t i = 0; i < ptyHandles.size(); ++i) {
|
|
||||||
pty_baton *ptyHandle = ptyHandles[i];
|
|
||||||
if (ptyHandle->id == id) {
|
|
||||||
return ptyHandle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
std::vector<T> vectorFromString(const std::basic_string<T> &str) {
|
|
||||||
return std::vector<T>(str.begin(), str.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
void throwNanError(const Nan::FunctionCallbackInfo<v8::Value> *info,
|
|
||||||
const char *text, const bool getLastError) {
|
|
||||||
std::stringstream errorText;
|
|
||||||
errorText << text;
|
|
||||||
if (getLastError) {
|
|
||||||
errorText << ", error code: " << GetLastError();
|
|
||||||
}
|
|
||||||
Nan::ThrowError(errorText.str().c_str());
|
|
||||||
(*info).GetReturnValue().SetUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a new server named pipe. It has not yet been connected.
|
|
||||||
bool createDataServerPipe(bool write, std::wstring kind, HANDLE *hServer,
|
|
||||||
std::wstring &name, const std::wstring &pipeName) {
|
|
||||||
*hServer = INVALID_HANDLE_VALUE;
|
|
||||||
|
|
||||||
name = L"\\\\.\\pipe\\" + pipeName + L"-" + kind;
|
|
||||||
|
|
||||||
const DWORD winOpenMode =
|
|
||||||
PIPE_ACCESS_INBOUND | PIPE_ACCESS_OUTBOUND |
|
|
||||||
FILE_FLAG_FIRST_PIPE_INSTANCE /* | FILE_FLAG_OVERLAPPED */;
|
|
||||||
|
|
||||||
SECURITY_ATTRIBUTES sa = {};
|
|
||||||
sa.nLength = sizeof(sa);
|
|
||||||
|
|
||||||
*hServer = CreateNamedPipeW(name.c_str(),
|
|
||||||
/*dwOpenMode=*/winOpenMode,
|
|
||||||
/*dwPipeMode=*/PIPE_TYPE_BYTE |
|
|
||||||
PIPE_READMODE_BYTE | PIPE_WAIT,
|
|
||||||
/*nMaxInstances=*/1,
|
|
||||||
/*nOutBufferSize=*/0,
|
|
||||||
/*nInBufferSize=*/0,
|
|
||||||
/*nDefaultTimeOut=*/30000, &sa);
|
|
||||||
|
|
||||||
return *hServer != INVALID_HANDLE_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
HRESULT CreateNamedPipesAndPseudoConsole(COORD size, DWORD dwFlags,
|
|
||||||
HANDLE *phInput, HANDLE *phOutput,
|
|
||||||
HPCON *phPC, std::wstring &inName,
|
|
||||||
std::wstring &outName,
|
|
||||||
const std::wstring &pipeName) {
|
|
||||||
HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
|
|
||||||
bool fLoadedDll = hLibrary != nullptr;
|
|
||||||
if (fLoadedDll) {
|
|
||||||
PFNCREATEPSEUDOCONSOLE const pfnCreate =
|
|
||||||
(PFNCREATEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,
|
|
||||||
"CreatePseudoConsole");
|
|
||||||
if (pfnCreate) {
|
|
||||||
if (phPC == NULL || phInput == NULL || phOutput == NULL) {
|
|
||||||
return E_INVALIDARG;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool success =
|
|
||||||
createDataServerPipe(true, L"in", phInput, inName, pipeName);
|
|
||||||
if (!success) {
|
|
||||||
return HRESULT_FROM_WIN32(GetLastError());
|
|
||||||
}
|
|
||||||
success =
|
|
||||||
createDataServerPipe(false, L"out", phOutput, outName, pipeName);
|
|
||||||
if (!success) {
|
|
||||||
return HRESULT_FROM_WIN32(GetLastError());
|
|
||||||
}
|
|
||||||
return pfnCreate(size, *phInput, *phOutput, dwFlags, phPC);
|
|
||||||
} else {
|
|
||||||
// Failed to find CreatePseudoConsole in kernel32. This is likely because
|
|
||||||
// the user is not running a build of Windows that supports that API.
|
|
||||||
// We should fall back to winpty in this case.
|
|
||||||
return HRESULT_FROM_WIN32(GetLastError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failed to find kernel32. This is really unlikely - honestly no idea how
|
|
||||||
// this is even possible to hit. But if it does happen, fall back to
|
|
||||||
// winpty.
|
|
||||||
return HRESULT_FROM_WIN32(GetLastError());
|
|
||||||
}
|
|
||||||
|
|
||||||
static NAN_METHOD(PtyStartProcess) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
|
|
||||||
v8::Local<v8::Object> marshal;
|
|
||||||
std::wstring inName, outName;
|
|
||||||
BOOL fSuccess = FALSE;
|
|
||||||
std::unique_ptr<wchar_t[]> mutableCommandline;
|
|
||||||
PROCESS_INFORMATION _piClient{};
|
|
||||||
|
|
||||||
if (info.Length() != 6 || !info[0]->IsString() || !info[1]->IsNumber() ||
|
|
||||||
!info[2]->IsNumber() || !info[3]->IsBoolean() || !info[4]->IsString() ||
|
|
||||||
!info[5]->IsBoolean()) {
|
|
||||||
Nan::ThrowError("Usage: pty.startProcess(file, cols, rows, debug, "
|
|
||||||
"pipeName, inheritCursor)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::wstring filename(path_util::to_wstring(Nan::Utf8String(info[0])));
|
|
||||||
const SHORT cols = info[1]->Uint32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
const SHORT rows = info[2]->Uint32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
const bool debug = Nan::To<bool>(info[3]).FromJust();
|
|
||||||
const std::wstring pipeName(path_util::to_wstring(Nan::Utf8String(info[4])));
|
|
||||||
const bool inheritCursor = Nan::To<bool>(info[5]).FromJust();
|
|
||||||
|
|
||||||
// use environment 'Path' variable to determine location of
|
|
||||||
// the relative path that we have received (e.g cmd.exe)
|
|
||||||
std::wstring shellpath;
|
|
||||||
if (::PathIsRelativeW(filename.c_str())) {
|
|
||||||
shellpath = path_util::get_shell_path(filename.c_str());
|
|
||||||
} else {
|
|
||||||
shellpath = filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string shellpath_(shellpath.begin(), shellpath.end());
|
|
||||||
|
|
||||||
if (shellpath.empty() || !path_util::file_exists(shellpath)) {
|
|
||||||
std::stringstream why;
|
|
||||||
why << "File not found: " << shellpath_;
|
|
||||||
Nan::ThrowError(why.str().c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
HANDLE hIn, hOut;
|
|
||||||
HPCON hpc;
|
|
||||||
HRESULT hr = CreateNamedPipesAndPseudoConsole(
|
|
||||||
{cols, rows}, inheritCursor ? 1 /*PSEUDOCONSOLE_INHERIT_CURSOR*/ : 0,
|
|
||||||
&hIn, &hOut, &hpc, inName, outName, pipeName);
|
|
||||||
|
|
||||||
// Restore default handling of ctrl+c
|
|
||||||
SetConsoleCtrlHandler(NULL, FALSE);
|
|
||||||
|
|
||||||
// Set return values
|
|
||||||
marshal = Nan::New<v8::Object>();
|
|
||||||
|
|
||||||
if (SUCCEEDED(hr)) {
|
|
||||||
// We were able to instantiate a conpty
|
|
||||||
const int ptyId = InterlockedIncrement(&ptyCounter);
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("pty").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>(ptyId));
|
|
||||||
ptyHandles.insert(ptyHandles.end(), new pty_baton(ptyId, hIn, hOut, hpc));
|
|
||||||
} else {
|
|
||||||
Nan::ThrowError("Cannot launch conpty");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("fd").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>(-1));
|
|
||||||
{
|
|
||||||
std::string coninPipeNameStr(inName.begin(), inName.end());
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("conin").ToLocalChecked(),
|
|
||||||
Nan::New<v8::String>(coninPipeNameStr).ToLocalChecked());
|
|
||||||
|
|
||||||
std::string conoutPipeNameStr(outName.begin(), outName.end());
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("conout").ToLocalChecked(),
|
|
||||||
Nan::New<v8::String>(conoutPipeNameStr).ToLocalChecked());
|
|
||||||
}
|
|
||||||
info.GetReturnValue().Set(marshal);
|
|
||||||
}
|
|
||||||
|
|
||||||
VOID CALLBACK OnProcessExitWinEvent(_In_ PVOID context,
|
|
||||||
_In_ BOOLEAN TimerOrWaitFired) {
|
|
||||||
pty_baton *baton = static_cast<pty_baton *>(context);
|
|
||||||
|
|
||||||
// Fire OnProcessExit
|
|
||||||
uv_async_send(&baton->async);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void OnProcessExit(uv_async_t *async) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
pty_baton *baton = static_cast<pty_baton *>(async->data);
|
|
||||||
|
|
||||||
UnregisterWait(baton->hWait);
|
|
||||||
|
|
||||||
// Get exit code
|
|
||||||
DWORD exitCode = 0;
|
|
||||||
GetExitCodeProcess(baton->hShell, &exitCode);
|
|
||||||
|
|
||||||
// Call function
|
|
||||||
v8::Local<v8::Value> args[1] = {Nan::New<v8::Number>(exitCode)};
|
|
||||||
|
|
||||||
Nan::AsyncResource asyncResource("node-pty.callback");
|
|
||||||
baton->cb.Call(1, args, &asyncResource);
|
|
||||||
// Clean up
|
|
||||||
baton->cb.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
static NAN_METHOD(PtyConnect) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
|
|
||||||
// If we're working with conpty's we need to call ConnectNamedPipe here AFTER
|
|
||||||
// the Socket has attempted to connect to the other end, then actually
|
|
||||||
// spawn the process here.
|
|
||||||
|
|
||||||
std::stringstream errorText;
|
|
||||||
BOOL fSuccess = FALSE;
|
|
||||||
|
|
||||||
if (info.Length() != 5 || !info[0]->IsNumber() || !info[1]->IsString() ||
|
|
||||||
!info[2]->IsString() || !info[3]->IsArray() || !info[4]->IsFunction()) {
|
|
||||||
Nan::ThrowError("Usage: pty.connect(id, cmdline, cwd, env, exitCallback)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int id = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
const std::wstring cmdline(path_util::to_wstring(Nan::Utf8String(info[1])));
|
|
||||||
const std::wstring cwd(path_util::to_wstring(Nan::Utf8String(info[2])));
|
|
||||||
const v8::Local<v8::Array> envValues = info[3].As<v8::Array>();
|
|
||||||
const v8::Local<v8::Function> exitCallback =
|
|
||||||
v8::Local<v8::Function>::Cast(info[4]);
|
|
||||||
|
|
||||||
// Prepare command line
|
|
||||||
std::unique_ptr<wchar_t[]> mutableCommandline =
|
|
||||||
std::make_unique<wchar_t[]>(cmdline.length() + 1);
|
|
||||||
HRESULT hr = StringCchCopyW(mutableCommandline.get(), cmdline.length() + 1,
|
|
||||||
cmdline.c_str());
|
|
||||||
|
|
||||||
// Prepare cwd
|
|
||||||
std::unique_ptr<wchar_t[]> mutableCwd =
|
|
||||||
std::make_unique<wchar_t[]>(cwd.length() + 1);
|
|
||||||
hr = StringCchCopyW(mutableCwd.get(), cwd.length() + 1, cwd.c_str());
|
|
||||||
|
|
||||||
// Prepare environment
|
|
||||||
std::wstring env;
|
|
||||||
if (!envValues.IsEmpty()) {
|
|
||||||
std::wstringstream envBlock;
|
|
||||||
for (uint32_t i = 0; i < envValues->Length(); i++) {
|
|
||||||
std::wstring envValue(path_util::to_wstring(
|
|
||||||
Nan::Utf8String(Nan::Get(envValues, i).ToLocalChecked())));
|
|
||||||
envBlock << envValue << L'\0';
|
|
||||||
}
|
|
||||||
envBlock << L'\0';
|
|
||||||
env = envBlock.str();
|
|
||||||
}
|
|
||||||
auto envV = vectorFromString(env);
|
|
||||||
LPWSTR envArg = envV.empty() ? nullptr : envV.data();
|
|
||||||
|
|
||||||
// Fetch pty handle from ID and start process
|
|
||||||
pty_baton *handle = get_pty_baton(id);
|
|
||||||
|
|
||||||
BOOL success = ConnectNamedPipe(handle->hIn, nullptr);
|
|
||||||
success = ConnectNamedPipe(handle->hOut, nullptr);
|
|
||||||
|
|
||||||
// Attach the pseudoconsole to the client application we're creating
|
|
||||||
STARTUPINFOEXW siEx{0};
|
|
||||||
siEx.StartupInfo.cb = sizeof(STARTUPINFOEXW);
|
|
||||||
siEx.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
|
|
||||||
siEx.StartupInfo.hStdError = nullptr;
|
|
||||||
siEx.StartupInfo.hStdInput = nullptr;
|
|
||||||
siEx.StartupInfo.hStdOutput = nullptr;
|
|
||||||
|
|
||||||
SIZE_T size = 0;
|
|
||||||
InitializeProcThreadAttributeList(NULL, 1, 0, &size);
|
|
||||||
BYTE *attrList = new BYTE[size];
|
|
||||||
siEx.lpAttributeList =
|
|
||||||
reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>(attrList);
|
|
||||||
|
|
||||||
fSuccess =
|
|
||||||
InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &size);
|
|
||||||
if (!fSuccess) {
|
|
||||||
return throwNanError(&info, "InitializeProcThreadAttributeList failed",
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
fSuccess = UpdateProcThreadAttribute(siEx.lpAttributeList, 0,
|
|
||||||
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
|
||||||
handle->hpc, sizeof(HPCON), NULL, NULL);
|
|
||||||
if (!fSuccess) {
|
|
||||||
return throwNanError(&info, "UpdateProcThreadAttribute failed", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
PROCESS_INFORMATION piClient{};
|
|
||||||
fSuccess = !!CreateProcessW(
|
|
||||||
nullptr, mutableCommandline.get(),
|
|
||||||
nullptr, // lpProcessAttributes
|
|
||||||
nullptr, // lpThreadAttributes
|
|
||||||
false, // bInheritHandles VERY IMPORTANT that this is false
|
|
||||||
EXTENDED_STARTUPINFO_PRESENT |
|
|
||||||
CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags
|
|
||||||
envArg, // lpEnvironment
|
|
||||||
mutableCwd.get(), // lpCurrentDirectory
|
|
||||||
&siEx.StartupInfo, // lpStartupInfo
|
|
||||||
&piClient // lpProcessInformation
|
|
||||||
);
|
|
||||||
if (!fSuccess) {
|
|
||||||
return throwNanError(&info, "Cannot create process", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update handle
|
|
||||||
handle->hShell = piClient.hProcess;
|
|
||||||
handle->cb.Reset(exitCallback);
|
|
||||||
handle->async.data = handle;
|
|
||||||
|
|
||||||
// Setup OnProcessExit callback
|
|
||||||
uv_async_init(uv_default_loop(), &handle->async, OnProcessExit);
|
|
||||||
|
|
||||||
// Setup Windows wait for process exit event
|
|
||||||
RegisterWaitForSingleObject(&handle->hWait, piClient.hProcess,
|
|
||||||
OnProcessExitWinEvent, (PVOID)handle, INFINITE,
|
|
||||||
WT_EXECUTEONLYONCE);
|
|
||||||
|
|
||||||
// Return
|
|
||||||
v8::Local<v8::Object> marshal = Nan::New<v8::Object>();
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("pid").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>(piClient.dwProcessId));
|
|
||||||
info.GetReturnValue().Set(marshal);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConPTY::resize(int id, int cols, int rows) {
|
|
||||||
// SHORT cols = info[1]->Uint32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
// SHORT rows = info[2]->Uint32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
|
|
||||||
const pty_baton *handle = get_pty_baton(id);
|
|
||||||
|
|
||||||
HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
|
|
||||||
bool fLoadedDll = hLibrary != nullptr;
|
|
||||||
if (fLoadedDll) {
|
|
||||||
PFNRESIZEPSEUDOCONSOLE const pfnResizePseudoConsole =
|
|
||||||
(PFNRESIZEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,
|
|
||||||
"ResizePseudoConsole");
|
|
||||||
if (pfnResizePseudoConsole) {
|
|
||||||
COORD size = {cols, rows};
|
|
||||||
pfnResizePseudoConsole(handle->hpc, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConPTY::kill(int id) {
|
|
||||||
const pty_baton *handle = get_pty_baton(id);
|
|
||||||
|
|
||||||
HANDLE hLibrary = LoadLibraryExW(L"kernel32.dll", 0, 0);
|
|
||||||
bool fLoadedDll = hLibrary != nullptr;
|
|
||||||
if (fLoadedDll) {
|
|
||||||
PFNCLOSEPSEUDOCONSOLE const pfnClosePseudoConsole =
|
|
||||||
(PFNCLOSEPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary,
|
|
||||||
"ClosePseudoConsole");
|
|
||||||
if (pfnClosePseudoConsole) {
|
|
||||||
pfnClosePseudoConsole(handle->hpc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CloseHandle(handle->hShell);
|
|
||||||
|
|
||||||
return info.GetReturnValue().SetUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init
|
|
||||||
*/
|
|
||||||
|
|
||||||
void ConPTY::_register_methods() {
|
|
||||||
register_method("_init", &ConPTY::_init);
|
|
||||||
register_method("start_process", &ConPTY::start_process);
|
|
||||||
register_method("connect_to_named_pipe", &ConPTY::connect_to_named_pipe);
|
|
||||||
register_method("resize", &ConPTY::resize);
|
|
||||||
register_method("kill", &ConPTY::kill);
|
|
||||||
};
|
|
||||||
|
|
||||||
void ConPTY::_init() {}
|
|
|
@ -1,36 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Leroy Hopson <godot-xterm@leroy.geek.nz>
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
#ifndef GODOT_XTERM_CONPTY_H
|
|
||||||
#define GODOT_XTERM_CONPTY_H
|
|
||||||
|
|
||||||
#include <godot_cpp/classes/ref_counted.hpp>
|
|
||||||
#include <godot_cpp/variant/callable.hpp>
|
|
||||||
|
|
||||||
namespace godot {
|
|
||||||
|
|
||||||
class ConPTY : public RefCounted {
|
|
||||||
GDCLASS(ConPTY, RefCounted)
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Array fork(String file,
|
|
||||||
// int _ignored, /* FIXME: For some reason Pipe throws
|
|
||||||
// ENOTSOCK in read callback if args (or another
|
|
||||||
// non-empty, non-zero) value is in this position. */
|
|
||||||
// PoolStringArray args, PoolStringArray env, String cwd, int
|
|
||||||
// cols, int rows, int uid, int gid, bool utf8, Ref<FuncRef>
|
|
||||||
// on_exit);
|
|
||||||
// Array open(int cols, int rows);
|
|
||||||
void resize(int id, int cols, int rows);
|
|
||||||
void kill(int id);
|
|
||||||
// String process(int fd, String tty);
|
|
||||||
|
|
||||||
void _init();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
static void _bind_methods();
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace godot
|
|
||||||
|
|
||||||
#endif // GODOT_XTERM_CONPTY_H
|
|
|
@ -1,43 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2019, Microsoft Corporation (MIT License).
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <nan.h>
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
static NAN_METHOD(ApiConsoleProcessList) {
|
|
||||||
if (info.Length() != 1 || !info[0]->IsNumber()) {
|
|
||||||
Nan::ThrowError("Usage: getConsoleProcessList(shellPid)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SHORT pid = info[0]->Uint32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
|
|
||||||
if (!FreeConsole()) {
|
|
||||||
Nan::ThrowError("FreeConsole failed");
|
|
||||||
}
|
|
||||||
if (!AttachConsole(pid)) {
|
|
||||||
Nan::ThrowError("AttachConsole failed");
|
|
||||||
}
|
|
||||||
auto processList = std::vector<DWORD>(64);
|
|
||||||
auto processCount =
|
|
||||||
GetConsoleProcessList(&processList[0], processList.size());
|
|
||||||
if (processList.size() < processCount) {
|
|
||||||
processList.resize(processCount);
|
|
||||||
processCount = GetConsoleProcessList(&processList[0], processList.size());
|
|
||||||
}
|
|
||||||
FreeConsole();
|
|
||||||
|
|
||||||
v8::Local<v8::Array> result = Nan::New<v8::Array>();
|
|
||||||
for (DWORD i = 0; i < processCount; i++) {
|
|
||||||
Nan::Set(result, i, Nan::New<v8::Number>(processList[i]));
|
|
||||||
}
|
|
||||||
info.GetReturnValue().Set(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void init(v8::Local<v8::Object> target) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
Nan::SetMethod(target, "getConsoleProcessList", ApiConsoleProcessList);
|
|
||||||
};
|
|
||||||
|
|
||||||
NODE_MODULE(pty, init);
|
|
|
@ -1,73 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
|
|
||||||
* Copyright (c) 2016, Daniel Imms (MIT License).
|
|
||||||
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Shlwapi.h> // PathCombine
|
|
||||||
#include <nan.h>
|
|
||||||
|
|
||||||
#include "path_util.h"
|
|
||||||
|
|
||||||
namespace path_util {
|
|
||||||
|
|
||||||
const wchar_t *to_wstring(const Nan::Utf8String &str) {
|
|
||||||
const char *bytes = *str;
|
|
||||||
unsigned int sizeOfStr = MultiByteToWideChar(CP_UTF8, 0, bytes, -1, NULL, 0);
|
|
||||||
wchar_t *output = new wchar_t[sizeOfStr];
|
|
||||||
MultiByteToWideChar(CP_UTF8, 0, bytes, -1, output, sizeOfStr);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool file_exists(std::wstring filename) {
|
|
||||||
DWORD attr = ::GetFileAttributesW(filename.c_str());
|
|
||||||
if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cmd.exe -> C:\Windows\system32\cmd.exe
|
|
||||||
std::wstring get_shell_path(std::wstring filename) {
|
|
||||||
std::wstring shellpath;
|
|
||||||
|
|
||||||
if (file_exists(filename)) {
|
|
||||||
return shellpath;
|
|
||||||
}
|
|
||||||
|
|
||||||
wchar_t buffer_[MAX_ENV];
|
|
||||||
int read = ::GetEnvironmentVariableW(L"Path", buffer_, MAX_ENV);
|
|
||||||
if (!read) {
|
|
||||||
return shellpath;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::wstring delimiter = L";";
|
|
||||||
size_t pos = 0;
|
|
||||||
std::vector<std::wstring> paths;
|
|
||||||
std::wstring buffer(buffer_);
|
|
||||||
while ((pos = buffer.find(delimiter)) != std::wstring::npos) {
|
|
||||||
paths.push_back(buffer.substr(0, pos));
|
|
||||||
buffer.erase(0, pos + delimiter.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
const wchar_t *filename_ = filename.c_str();
|
|
||||||
|
|
||||||
for (int i = 0; i < paths.size(); ++i) {
|
|
||||||
std::wstring path = paths[i];
|
|
||||||
wchar_t searchPath[MAX_PATH];
|
|
||||||
::PathCombineW(searchPath, const_cast<wchar_t *>(path.c_str()), filename_);
|
|
||||||
|
|
||||||
if (searchPath == NULL) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_exists(searchPath)) {
|
|
||||||
shellpath = searchPath;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return shellpath;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace path_util
|
|
|
@ -1,22 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
|
|
||||||
* Copyright (c) 2016, Daniel Imms (MIT License).
|
|
||||||
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef NODE_PTY_PATH_UTIL_H_
|
|
||||||
#define NODE_PTY_PATH_UTIL_H_
|
|
||||||
|
|
||||||
#include <nan.h>
|
|
||||||
|
|
||||||
#define MAX_ENV 65536
|
|
||||||
|
|
||||||
namespace path_util {
|
|
||||||
|
|
||||||
const wchar_t *to_wstring(const Nan::Utf8String &str);
|
|
||||||
bool file_exists(std::wstring filename);
|
|
||||||
std::wstring get_shell_path(std::wstring filename);
|
|
||||||
|
|
||||||
} // namespace path_util
|
|
||||||
|
|
||||||
#endif // NODE_PTY_PATH_UTIL_H_
|
|
|
@ -1,320 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2013-2015, Christopher Jeffrey, Peter Sunde (MIT License)
|
|
||||||
* Copyright (c) 2016, Daniel Imms (MIT License).
|
|
||||||
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
|
||||||
*
|
|
||||||
* pty.cc:
|
|
||||||
* This file is responsible for starting processes
|
|
||||||
* with pseudo-terminal file descriptors.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Shlwapi.h> // PathCombine, PathIsRelative
|
|
||||||
#include <iostream>
|
|
||||||
#include <nan.h>
|
|
||||||
#include <sstream>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <winpty.h>
|
|
||||||
|
|
||||||
#include "path_util.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Misc
|
|
||||||
*/
|
|
||||||
extern "C" void init(v8::Local<v8::Object>);
|
|
||||||
|
|
||||||
#define WINPTY_DBG_VARIABLE TEXT("WINPTYDBG")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* winpty
|
|
||||||
*/
|
|
||||||
static std::vector<winpty_t *> ptyHandles;
|
|
||||||
static volatile LONG ptyCounter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helpers
|
|
||||||
*/
|
|
||||||
|
|
||||||
static winpty_t *get_pipe_handle(int handle) {
|
|
||||||
for (size_t i = 0; i < ptyHandles.size(); ++i) {
|
|
||||||
winpty_t *ptyHandle = ptyHandles[i];
|
|
||||||
int current = (int)winpty_agent_process(ptyHandle);
|
|
||||||
if (current == handle) {
|
|
||||||
return ptyHandle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool remove_pipe_handle(int handle) {
|
|
||||||
for (size_t i = 0; i < ptyHandles.size(); ++i) {
|
|
||||||
winpty_t *ptyHandle = ptyHandles[i];
|
|
||||||
if ((int)winpty_agent_process(ptyHandle) == handle) {
|
|
||||||
winpty_free(ptyHandle);
|
|
||||||
ptyHandles.erase(ptyHandles.begin() + i);
|
|
||||||
ptyHandle = nullptr;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void throw_winpty_error(const char *generalMsg, winpty_error_ptr_t error_ptr) {
|
|
||||||
std::stringstream why;
|
|
||||||
std::wstring msg(winpty_error_msg(error_ptr));
|
|
||||||
std::string msg_(msg.begin(), msg.end());
|
|
||||||
why << generalMsg << ": " << msg_;
|
|
||||||
Nan::ThrowError(why.str().c_str());
|
|
||||||
winpty_error_free(error_ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
static NAN_METHOD(PtyGetExitCode) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
|
|
||||||
if (info.Length() != 1 || !info[0]->IsNumber()) {
|
|
||||||
Nan::ThrowError("Usage: pty.getExitCode(pidHandle)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DWORD exitCode = 0;
|
|
||||||
GetExitCodeProcess(
|
|
||||||
(HANDLE)info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust(),
|
|
||||||
&exitCode);
|
|
||||||
|
|
||||||
info.GetReturnValue().Set(Nan::New<v8::Number>(exitCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
static NAN_METHOD(PtyGetProcessList) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
|
|
||||||
if (info.Length() != 1 || !info[0]->IsNumber()) {
|
|
||||||
Nan::ThrowError("Usage: pty.getProcessList(pid)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int pid = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
|
|
||||||
winpty_t *pc = get_pipe_handle(pid);
|
|
||||||
if (pc == nullptr) {
|
|
||||||
info.GetReturnValue().Set(Nan::New<v8::Array>(0));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int processList[64];
|
|
||||||
const int processCount = 64;
|
|
||||||
int actualCount =
|
|
||||||
winpty_get_console_process_list(pc, processList, processCount, nullptr);
|
|
||||||
|
|
||||||
v8::Local<v8::Array> result = Nan::New<v8::Array>(actualCount);
|
|
||||||
for (uint32_t i = 0; i < actualCount; i++) {
|
|
||||||
Nan::Set(result, i, Nan::New<v8::Number>(processList[i]));
|
|
||||||
}
|
|
||||||
info.GetReturnValue().Set(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
static NAN_METHOD(PtyStartProcess) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
|
|
||||||
if (info.Length() != 7 || !info[0]->IsString() || !info[1]->IsString() ||
|
|
||||||
!info[2]->IsArray() || !info[3]->IsString() || !info[4]->IsNumber() ||
|
|
||||||
!info[5]->IsNumber() || !info[6]->IsBoolean()) {
|
|
||||||
Nan::ThrowError(
|
|
||||||
"Usage: pty.startProcess(file, cmdline, env, cwd, cols, rows, debug)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::stringstream why;
|
|
||||||
|
|
||||||
const wchar_t *filename = path_util::to_wstring(Nan::Utf8String(info[0]));
|
|
||||||
const wchar_t *cmdline = path_util::to_wstring(Nan::Utf8String(info[1]));
|
|
||||||
const wchar_t *cwd = path_util::to_wstring(Nan::Utf8String(info[3]));
|
|
||||||
|
|
||||||
// create environment block
|
|
||||||
std::wstring env;
|
|
||||||
const v8::Local<v8::Array> envValues = v8::Local<v8::Array>::Cast(info[2]);
|
|
||||||
if (!envValues.IsEmpty()) {
|
|
||||||
|
|
||||||
std::wstringstream envBlock;
|
|
||||||
|
|
||||||
for (uint32_t i = 0; i < envValues->Length(); i++) {
|
|
||||||
std::wstring envValue(path_util::to_wstring(
|
|
||||||
Nan::Utf8String(Nan::Get(envValues, i).ToLocalChecked())));
|
|
||||||
envBlock << envValue << L'\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
env = envBlock.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
// use environment 'Path' variable to determine location of
|
|
||||||
// the relative path that we have received (e.g cmd.exe)
|
|
||||||
std::wstring shellpath;
|
|
||||||
if (::PathIsRelativeW(filename)) {
|
|
||||||
shellpath = path_util::get_shell_path(filename);
|
|
||||||
} else {
|
|
||||||
shellpath = filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string shellpath_(shellpath.begin(), shellpath.end());
|
|
||||||
|
|
||||||
if (shellpath.empty() || !path_util::file_exists(shellpath)) {
|
|
||||||
why << "File not found: " << shellpath_;
|
|
||||||
Nan::ThrowError(why.str().c_str());
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
|
|
||||||
int cols = info[4]->Int32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
int rows = info[5]->Int32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
bool debug = Nan::To<bool>(info[6]).FromJust();
|
|
||||||
|
|
||||||
// Enable/disable debugging
|
|
||||||
SetEnvironmentVariable(WINPTY_DBG_VARIABLE,
|
|
||||||
debug ? "1" : NULL); // NULL = deletes variable
|
|
||||||
|
|
||||||
// Create winpty config
|
|
||||||
winpty_error_ptr_t error_ptr = nullptr;
|
|
||||||
winpty_config_t *winpty_config = winpty_config_new(0, &error_ptr);
|
|
||||||
if (winpty_config == nullptr) {
|
|
||||||
throw_winpty_error("Error creating WinPTY config", error_ptr);
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
winpty_error_free(error_ptr);
|
|
||||||
|
|
||||||
// Set pty size on config
|
|
||||||
winpty_config_set_initial_size(winpty_config, cols, rows);
|
|
||||||
|
|
||||||
// Start the pty agent
|
|
||||||
winpty_t *pc = winpty_open(winpty_config, &error_ptr);
|
|
||||||
winpty_config_free(winpty_config);
|
|
||||||
if (pc == nullptr) {
|
|
||||||
throw_winpty_error("Error launching WinPTY agent", error_ptr);
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
winpty_error_free(error_ptr);
|
|
||||||
|
|
||||||
// Save pty struct for later use
|
|
||||||
ptyHandles.insert(ptyHandles.end(), pc);
|
|
||||||
|
|
||||||
// Create winpty spawn config
|
|
||||||
winpty_spawn_config_t *config = winpty_spawn_config_new(
|
|
||||||
WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, shellpath.c_str(), cmdline, cwd,
|
|
||||||
env.c_str(), &error_ptr);
|
|
||||||
if (config == nullptr) {
|
|
||||||
throw_winpty_error("Error creating WinPTY spawn config", error_ptr);
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
winpty_error_free(error_ptr);
|
|
||||||
|
|
||||||
// Spawn the new process
|
|
||||||
HANDLE handle = nullptr;
|
|
||||||
BOOL spawnSuccess =
|
|
||||||
winpty_spawn(pc, config, &handle, nullptr, nullptr, &error_ptr);
|
|
||||||
winpty_spawn_config_free(config);
|
|
||||||
if (!spawnSuccess) {
|
|
||||||
throw_winpty_error("Unable to start terminal process", error_ptr);
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
winpty_error_free(error_ptr);
|
|
||||||
|
|
||||||
// Set return values
|
|
||||||
v8::Local<v8::Object> marshal = Nan::New<v8::Object>();
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("innerPid").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>((int)GetProcessId(handle)));
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("innerPidHandle").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>((int)handle));
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("pid").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>((int)winpty_agent_process(pc)));
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("pty").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>(InterlockedIncrement(&ptyCounter)));
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("fd").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>(-1));
|
|
||||||
{
|
|
||||||
LPCWSTR coninPipeName = winpty_conin_name(pc);
|
|
||||||
std::wstring coninPipeNameWStr(coninPipeName);
|
|
||||||
std::string coninPipeNameStr(coninPipeNameWStr.begin(),
|
|
||||||
coninPipeNameWStr.end());
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("conin").ToLocalChecked(),
|
|
||||||
Nan::New<v8::String>(coninPipeNameStr).ToLocalChecked());
|
|
||||||
LPCWSTR conoutPipeName = winpty_conout_name(pc);
|
|
||||||
std::wstring conoutPipeNameWStr(conoutPipeName);
|
|
||||||
std::string conoutPipeNameStr(conoutPipeNameWStr.begin(),
|
|
||||||
conoutPipeNameWStr.end());
|
|
||||||
Nan::Set(marshal, Nan::New<v8::String>("conout").ToLocalChecked(),
|
|
||||||
Nan::New<v8::String>(conoutPipeNameStr).ToLocalChecked());
|
|
||||||
}
|
|
||||||
info.GetReturnValue().Set(marshal);
|
|
||||||
|
|
||||||
goto cleanup;
|
|
||||||
|
|
||||||
cleanup:
|
|
||||||
delete filename;
|
|
||||||
delete cmdline;
|
|
||||||
delete cwd;
|
|
||||||
}
|
|
||||||
|
|
||||||
static NAN_METHOD(PtyResize) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
|
|
||||||
if (info.Length() != 3 || !info[0]->IsNumber() || !info[1]->IsNumber() ||
|
|
||||||
!info[2]->IsNumber()) {
|
|
||||||
Nan::ThrowError("Usage: pty.resize(pid, cols, rows)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int handle = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
int cols = info[1]->Int32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
int rows = info[2]->Int32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
|
|
||||||
winpty_t *pc = get_pipe_handle(handle);
|
|
||||||
|
|
||||||
if (pc == nullptr) {
|
|
||||||
Nan::ThrowError("The pty doesn't appear to exist");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BOOL success = winpty_set_size(pc, cols, rows, nullptr);
|
|
||||||
if (!success) {
|
|
||||||
Nan::ThrowError("The pty could not be resized");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return info.GetReturnValue().SetUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
static NAN_METHOD(PtyKill) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
|
|
||||||
if (info.Length() != 2 || !info[0]->IsNumber() || !info[1]->IsNumber()) {
|
|
||||||
Nan::ThrowError("Usage: pty.kill(pid, innerPidHandle)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int handle = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
HANDLE innerPidHandle =
|
|
||||||
(HANDLE)info[1]->Int32Value(Nan::GetCurrentContext()).FromJust();
|
|
||||||
|
|
||||||
winpty_t *pc = get_pipe_handle(handle);
|
|
||||||
if (pc == nullptr) {
|
|
||||||
Nan::ThrowError("Pty seems to have been killed already");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(remove_pipe_handle(handle));
|
|
||||||
CloseHandle(innerPidHandle);
|
|
||||||
|
|
||||||
return info.GetReturnValue().SetUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init
|
|
||||||
*/
|
|
||||||
|
|
||||||
extern "C" void init(v8::Local<v8::Object> target) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
Nan::SetMethod(target, "startProcess", PtyStartProcess);
|
|
||||||
Nan::SetMethod(target, "resize", PtyResize);
|
|
||||||
Nan::SetMethod(target, "kill", PtyKill);
|
|
||||||
Nan::SetMethod(target, "getExitCode", PtyGetExitCode);
|
|
||||||
Nan::SetMethod(target, "getProcessList", PtyGetProcessList);
|
|
||||||
};
|
|
||||||
|
|
||||||
NODE_MODULE(pty, init);
|
|
|
@ -1,134 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021-2023 Leroy Hopson <godot-xterm@leroy.geek.nz>
|
|
||||||
// SDPX-License-Identifier: MIT
|
|
||||||
|
|
||||||
#include "pipe.h"
|
|
||||||
#include "libuv_utils.h"
|
|
||||||
|
|
||||||
#ifndef ULONG
|
|
||||||
#define ULONG size_t
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using namespace godot;
|
|
||||||
|
|
||||||
void Pipe::_bind_methods() {
|
|
||||||
ClassDB::bind_method(D_METHOD("_init"), &Pipe::_init);
|
|
||||||
|
|
||||||
ClassDB::bind_method(D_METHOD("poll"), &Pipe::_poll_connection);
|
|
||||||
ClassDB::bind_method(D_METHOD("open", "fd", "ipc"), &Pipe::open);
|
|
||||||
ClassDB::bind_method(D_METHOD("write"), &Pipe::write);
|
|
||||||
ClassDB::bind_method(D_METHOD("get_status"), &Pipe::get_status);
|
|
||||||
ClassDB::bind_method(D_METHOD("close"), &Pipe::close);
|
|
||||||
|
|
||||||
ADD_SIGNAL(MethodInfo("data_received",
|
|
||||||
PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
|
|
||||||
}
|
|
||||||
|
|
||||||
Pipe::Pipe() {}
|
|
||||||
Pipe::~Pipe() { close(); }
|
|
||||||
|
|
||||||
void Pipe::_init() {}
|
|
||||||
|
|
||||||
void _poll_connection();
|
|
||||||
|
|
||||||
void _read_cb(uv_stream_t *handle, ssize_t nread, const uv_buf_t *buf);
|
|
||||||
|
|
||||||
void _close_cb(uv_handle_t *handle);
|
|
||||||
|
|
||||||
void _write_cb(uv_write_t *req, int status);
|
|
||||||
|
|
||||||
void _alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf);
|
|
||||||
|
|
||||||
Error Pipe::open(int fd, bool ipc = false) {
|
|
||||||
RETURN_IF_UV_ERR(uv_pipe_init(uv_default_loop(), &handle, ipc));
|
|
||||||
|
|
||||||
handle.data = this;
|
|
||||||
|
|
||||||
RETURN_IF_UV_ERR(uv_pipe_open(&handle, fd));
|
|
||||||
RETURN_IF_UV_ERR(uv_stream_set_blocking((uv_stream_t *)&handle, false));
|
|
||||||
RETURN_IF_UV_ERR(
|
|
||||||
uv_read_start((uv_stream_t *)&handle, _alloc_buffer, _read_cb));
|
|
||||||
|
|
||||||
status = 1;
|
|
||||||
return OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pipe::close() {
|
|
||||||
uv_close((uv_handle_t *)&handle, _close_cb);
|
|
||||||
uv_run(uv_default_loop(), UV_RUN_NOWAIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
Error Pipe::write(PackedByteArray data) {
|
|
||||||
char *s = (char *)data.ptr();
|
|
||||||
ULONG len = data.size();
|
|
||||||
|
|
||||||
uv_buf_t buf;
|
|
||||||
uv_write_t *req = (uv_write_t *)malloc(sizeof(uv_write_t));
|
|
||||||
|
|
||||||
buf.base = s;
|
|
||||||
buf.len = len;
|
|
||||||
req->data = (void *)buf.base;
|
|
||||||
|
|
||||||
uv_write(req, (uv_stream_t *)&handle, &buf, 1, _write_cb);
|
|
||||||
uv_run(uv_default_loop(), UV_RUN_NOWAIT);
|
|
||||||
|
|
||||||
return OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
int Pipe::get_status() {
|
|
||||||
if (!uv_is_active((uv_handle_t *)&handle))
|
|
||||||
status = 0;
|
|
||||||
_poll_connection();
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pipe::_poll_connection() {
|
|
||||||
if (status == 1 && !uv_is_active((uv_handle_t *)&handle))
|
|
||||||
uv_read_start((uv_stream_t *)&handle, _alloc_buffer, _read_cb);
|
|
||||||
|
|
||||||
uv_run(uv_default_loop(), UV_RUN_NOWAIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _read_cb(uv_stream_t *handle, ssize_t nread, const uv_buf_t *buf) {
|
|
||||||
Pipe *pipe = static_cast<Pipe *>(handle->data);
|
|
||||||
|
|
||||||
if (nread < 0) {
|
|
||||||
switch (nread) {
|
|
||||||
case UV_EOF:
|
|
||||||
// Normal after shell exits.
|
|
||||||
case UV_EIO:
|
|
||||||
// Can happen when the process exits.
|
|
||||||
// As long as PTY has caught it, we should be fine.
|
|
||||||
uv_read_stop(handle);
|
|
||||||
pipe->status = 0;
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
UV_ERR_PRINT(nread);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PackedByteArray data;
|
|
||||||
data.resize(nread);
|
|
||||||
{ memcpy(data.ptrw(), buf->base, nread); }
|
|
||||||
std::free((char *)buf->base);
|
|
||||||
|
|
||||||
pipe->emit_signal("data_received", data);
|
|
||||||
|
|
||||||
// Stop reading until the next poll, otherwise _read_cb could be called
|
|
||||||
// repeatedly, blocking Godot, and eventually resulting in a memory pool
|
|
||||||
// allocation error. This can be triggered with the command `cat /dev/urandom`
|
|
||||||
// if reading is not stopped.
|
|
||||||
uv_read_stop(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _write_cb(uv_write_t *req, int status) { std::free(req); }
|
|
||||||
|
|
||||||
void _alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
|
|
||||||
buf->base = (char *)malloc(suggested_size);
|
|
||||||
buf->len = suggested_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _close_cb(uv_handle_t *handle) {
|
|
||||||
Pipe *pipe = static_cast<Pipe *>(handle->data);
|
|
||||||
pipe->status = 0;
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021-2022 Leroy Hopson <godot-xterm@leroy.geek.nz>
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
#ifndef GODOT_XTERM_PIPE_H
|
|
||||||
#define GODOT_XTERM_PIPE_H
|
|
||||||
|
|
||||||
#include <godot_cpp/classes/ref_counted.hpp>
|
|
||||||
#include <godot_cpp/variant/packed_byte_array.hpp>
|
|
||||||
#include <uv.h>
|
|
||||||
|
|
||||||
namespace godot {
|
|
||||||
|
|
||||||
class Pipe : public RefCounted {
|
|
||||||
GDCLASS(Pipe, RefCounted)
|
|
||||||
|
|
||||||
public:
|
|
||||||
uv_pipe_t handle;
|
|
||||||
|
|
||||||
Pipe();
|
|
||||||
~Pipe();
|
|
||||||
|
|
||||||
void _init();
|
|
||||||
|
|
||||||
Error open(int fd, bool ipc);
|
|
||||||
void close();
|
|
||||||
int get_status();
|
|
||||||
|
|
||||||
Error write(PackedByteArray data);
|
|
||||||
|
|
||||||
void pause();
|
|
||||||
void resume();
|
|
||||||
|
|
||||||
public:
|
|
||||||
int status;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
static void _bind_methods();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void _poll_connection();
|
|
||||||
|
|
||||||
static Error _translate_error(int err);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace godot
|
|
||||||
|
|
||||||
#endif // GODOT_XTERM_PIPE_H
|
|
|
@ -1,680 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
|
||||||
* Copyright (c) 2017, Daniel Imms (MIT License)
|
|
||||||
* Copyright (c) 2021, 2024 Leroy Hopson (MIT License)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*
|
|
||||||
* pty.cc:
|
|
||||||
* This file is responsible for starting processes
|
|
||||||
* with pseudo-terminal file descriptors.
|
|
||||||
*
|
|
||||||
* See:
|
|
||||||
* man pty
|
|
||||||
* man tty_ioctl
|
|
||||||
* man termios
|
|
||||||
* man forkpty
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Includes
|
|
||||||
*/
|
|
||||||
|
|
||||||
#if !defined(_WIN32) && !defined(_PTY_DISABLED)
|
|
||||||
|
|
||||||
#include "pty_unix.h"
|
|
||||||
#include <godot_cpp/variant/callable.hpp>
|
|
||||||
#include <uv.h>
|
|
||||||
|
|
||||||
#include <errno.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <sys/ioctl.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
|
|
||||||
/* forkpty */
|
|
||||||
/* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */
|
|
||||||
#if defined(__GLIBC__) || defined(__CYGWIN__)
|
|
||||||
#include <pty.h>
|
|
||||||
#elif defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__)
|
|
||||||
#include <util.h>
|
|
||||||
#elif defined(__FreeBSD__)
|
|
||||||
#include <libutil.h>
|
|
||||||
#elif defined(__sun)
|
|
||||||
#include <stropts.h> /* for I_PUSH */
|
|
||||||
#else
|
|
||||||
#include <pty.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <termios.h> /* tcgetattr, tty_ioctl */
|
|
||||||
|
|
||||||
/* Some platforms name VWERASE and VDISCARD differently */
|
|
||||||
#if !defined(VWERASE) && defined(VWERSE)
|
|
||||||
#define VWERASE VWERSE
|
|
||||||
#endif
|
|
||||||
#if !defined(VDISCARD) && defined(VDISCRD)
|
|
||||||
#define VDISCARD VDISCRD
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* environ for execvpe */
|
|
||||||
/* node/src/node_child_process.cc */
|
|
||||||
#if defined(__APPLE__) && !TARGET_OS_IPHONE
|
|
||||||
#include <crt_externs.h>
|
|
||||||
#define environ (*_NSGetEnviron())
|
|
||||||
#else
|
|
||||||
extern char **environ;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* for pty_getproc */
|
|
||||||
#if defined(__linux__)
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
#include <libproc.h>
|
|
||||||
#include <sys/sysctl.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* NSIG - macro for highest signal + 1, should be defined */
|
|
||||||
#ifndef NSIG
|
|
||||||
#define NSIG 32
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using namespace godot;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Structs
|
|
||||||
*/
|
|
||||||
|
|
||||||
struct pty_baton {
|
|
||||||
Callable cb;
|
|
||||||
int exit_code;
|
|
||||||
int signal_code;
|
|
||||||
pid_t pid;
|
|
||||||
uv_async_t async;
|
|
||||||
uv_thread_t tid;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int pty_execvpe(const char *, char **, char **);
|
|
||||||
|
|
||||||
static int pty_nonblock(int);
|
|
||||||
|
|
||||||
static char *pty_getproc(int, char *);
|
|
||||||
|
|
||||||
static int pty_openpty(int *, int *, char *, const struct termios *,
|
|
||||||
const struct winsize *);
|
|
||||||
|
|
||||||
static pid_t pty_forkpty(int *, char *, const struct termios *,
|
|
||||||
const struct winsize *);
|
|
||||||
|
|
||||||
static void pty_waitpid(void *);
|
|
||||||
|
|
||||||
static void pty_after_waitpid(uv_async_t *);
|
|
||||||
|
|
||||||
static void pty_after_close(uv_handle_t *);
|
|
||||||
|
|
||||||
Array PTYUnix::fork(String p_file, int _ignored, PackedStringArray p_args,
|
|
||||||
PackedStringArray p_env, String p_cwd, int p_cols,
|
|
||||||
int p_rows, int p_uid, int p_gid, bool p_utf8,
|
|
||||||
Callable p_on_exit) {
|
|
||||||
// file
|
|
||||||
char *file = strdup(p_file.utf8().get_data());
|
|
||||||
|
|
||||||
// args
|
|
||||||
int i = 0;
|
|
||||||
int argc = p_args.size();
|
|
||||||
int argl = argc + 1 + 1;
|
|
||||||
char **argv = new char *[argl];
|
|
||||||
argv[0] = strdup(file);
|
|
||||||
argv[argl - 1] = NULL;
|
|
||||||
for (; i < argc; i++) {
|
|
||||||
char *arg = strdup(p_args[i].utf8().get_data());
|
|
||||||
argv[i + 1] = strdup(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// env
|
|
||||||
i = 0;
|
|
||||||
int envc = p_env.size();
|
|
||||||
char **env = new char *[envc + 1];
|
|
||||||
env[envc] = NULL;
|
|
||||||
for (; i < envc; i++) {
|
|
||||||
char *pairs = strdup(p_env[i].utf8().get_data());
|
|
||||||
env[i] = strdup(pairs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cwd
|
|
||||||
char *cwd = strdup(p_cwd.utf8().get_data());
|
|
||||||
|
|
||||||
// size
|
|
||||||
struct winsize winp;
|
|
||||||
winp.ws_col = p_cols;
|
|
||||||
winp.ws_row = p_rows;
|
|
||||||
winp.ws_xpixel = 0;
|
|
||||||
winp.ws_ypixel = 0;
|
|
||||||
|
|
||||||
// termios
|
|
||||||
struct termios t = termios();
|
|
||||||
struct termios *term = &t;
|
|
||||||
term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT;
|
|
||||||
if (p_utf8) {
|
|
||||||
#if defined(IUTF8)
|
|
||||||
term->c_iflag |= IUTF8;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
term->c_oflag = OPOST | ONLCR;
|
|
||||||
term->c_cflag = CREAD | CS8 | HUPCL;
|
|
||||||
term->c_lflag =
|
|
||||||
ICANON | ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOKE | ECHOCTL;
|
|
||||||
|
|
||||||
term->c_cc[VEOF] = 4;
|
|
||||||
term->c_cc[VEOL] = -1;
|
|
||||||
term->c_cc[VEOL2] = -1;
|
|
||||||
term->c_cc[VERASE] = 0x7f;
|
|
||||||
term->c_cc[VWERASE] = 23;
|
|
||||||
term->c_cc[VKILL] = 21;
|
|
||||||
term->c_cc[VREPRINT] = 18;
|
|
||||||
term->c_cc[VINTR] = 3;
|
|
||||||
term->c_cc[VQUIT] = 0x1c;
|
|
||||||
term->c_cc[VSUSP] = 26;
|
|
||||||
term->c_cc[VSTART] = 17;
|
|
||||||
term->c_cc[VSTOP] = 19;
|
|
||||||
term->c_cc[VLNEXT] = 22;
|
|
||||||
term->c_cc[VDISCARD] = 15;
|
|
||||||
term->c_cc[VMIN] = 1;
|
|
||||||
term->c_cc[VTIME] = 0;
|
|
||||||
|
|
||||||
#if (__APPLE__)
|
|
||||||
term->c_cc[VDSUSP] = 25;
|
|
||||||
term->c_cc[VSTATUS] = 20;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
cfsetispeed(term, B38400);
|
|
||||||
cfsetospeed(term, B38400);
|
|
||||||
|
|
||||||
// uid / gid
|
|
||||||
int uid = p_uid;
|
|
||||||
int gid = p_gid;
|
|
||||||
|
|
||||||
// fork the pty
|
|
||||||
int master = -1;
|
|
||||||
|
|
||||||
sigset_t newmask, oldmask;
|
|
||||||
struct sigaction sig_action;
|
|
||||||
|
|
||||||
// temporarily block all signals
|
|
||||||
// this is needed due to a race condition in openpty
|
|
||||||
// and to avoid running signal handlers in the child
|
|
||||||
// before exec* happened
|
|
||||||
sigfillset(&newmask);
|
|
||||||
pthread_sigmask(SIG_SETMASK, &newmask, &oldmask);
|
|
||||||
|
|
||||||
pid_t pid = pty_forkpty(&master, nullptr, term, &winp);
|
|
||||||
|
|
||||||
if (!pid) {
|
|
||||||
// remove all signal handler from child
|
|
||||||
sig_action.sa_handler = SIG_DFL;
|
|
||||||
sig_action.sa_flags = 0;
|
|
||||||
sigemptyset(&sig_action.sa_mask);
|
|
||||||
for (int i = 0; i < NSIG; i++) { // NSIG is a macro for all signals + 1
|
|
||||||
sigaction(i, &sig_action, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// re-enable signals
|
|
||||||
pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
|
|
||||||
|
|
||||||
if (pid) {
|
|
||||||
for (i = 0; i < argl; i++)
|
|
||||||
std::free(argv[i]);
|
|
||||||
delete[] argv;
|
|
||||||
for (i = 0; i < envc; i++)
|
|
||||||
std::free(env[i]);
|
|
||||||
delete[] env;
|
|
||||||
std::free(cwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (pid) {
|
|
||||||
case -1:
|
|
||||||
ERR_PRINT("forkpty(3) failed.");
|
|
||||||
return Array::make(FAILED);
|
|
||||||
case 0:
|
|
||||||
if (strlen(cwd)) {
|
|
||||||
if (chdir(cwd) == -1) {
|
|
||||||
perror("chdir(2) failed.");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uid != -1 && gid != -1) {
|
|
||||||
if (setgid(gid) == -1) {
|
|
||||||
perror("setgid(2) failed.");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
if (setuid(uid) == -1) {
|
|
||||||
perror("setuid(2) failed.");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pty_execvpe(argv[0], argv, env);
|
|
||||||
|
|
||||||
perror("execvp(3) failed.");
|
|
||||||
_exit(1);
|
|
||||||
default:
|
|
||||||
if (pty_nonblock(master) == -1) {
|
|
||||||
ERR_PRINT("Could not set master fd to nonblocking.");
|
|
||||||
return Array::make(FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
Dictionary result;
|
|
||||||
result["fd"] = (int)master;
|
|
||||||
result["pid"] = (int)pid;
|
|
||||||
result["pty"] = ptsname(master);
|
|
||||||
|
|
||||||
pty_baton *baton = new pty_baton();
|
|
||||||
baton->exit_code = 0;
|
|
||||||
baton->signal_code = 0;
|
|
||||||
baton->cb = p_on_exit;
|
|
||||||
baton->pid = pid;
|
|
||||||
baton->async.data = baton;
|
|
||||||
|
|
||||||
uv_async_init(uv_default_loop(), &baton->async, pty_after_waitpid);
|
|
||||||
|
|
||||||
uv_thread_create(&baton->tid, pty_waitpid, static_cast<void *>(baton));
|
|
||||||
|
|
||||||
return Array::make(OK, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array::make(FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
Array PTYUnix::open(int p_cols, int p_rows) {
|
|
||||||
// size
|
|
||||||
struct winsize winp;
|
|
||||||
winp.ws_col = p_cols;
|
|
||||||
winp.ws_row = p_rows;
|
|
||||||
winp.ws_xpixel = 0;
|
|
||||||
winp.ws_ypixel = 0;
|
|
||||||
|
|
||||||
// pty
|
|
||||||
int master, slave;
|
|
||||||
int ret = pty_openpty(&master, &slave, nullptr, NULL, &winp);
|
|
||||||
|
|
||||||
if (ret == -1) {
|
|
||||||
ERR_PRINT("openpty(3) failed.");
|
|
||||||
return Array::make(FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pty_nonblock(master) == -1) {
|
|
||||||
ERR_PRINT("Could not set master fd to nonblocking.");
|
|
||||||
return Array::make(FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pty_nonblock(slave) == -1) {
|
|
||||||
ERR_PRINT("Could not set slave fd to nonblocking.");
|
|
||||||
return Array::make(FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
Dictionary dict;
|
|
||||||
dict["master"] = master;
|
|
||||||
dict["slave"] = slave;
|
|
||||||
dict["pty"] = ptsname(master);
|
|
||||||
|
|
||||||
return Array::make(OK, dict);
|
|
||||||
}
|
|
||||||
|
|
||||||
Error PTYUnix::resize(int p_fd, int p_cols, int p_rows) {
|
|
||||||
int fd = p_fd;
|
|
||||||
|
|
||||||
struct winsize winp;
|
|
||||||
winp.ws_col = p_cols;
|
|
||||||
winp.ws_row = p_rows;
|
|
||||||
winp.ws_xpixel = 0;
|
|
||||||
winp.ws_ypixel = 0;
|
|
||||||
|
|
||||||
if (ioctl(fd, TIOCSWINSZ, &winp) == -1) {
|
|
||||||
switch (errno) {
|
|
||||||
// TODO: Fixme!
|
|
||||||
//case EBADF:
|
|
||||||
// RETURN_UV_ERR(UV_EBADF)
|
|
||||||
//case EFAULT:
|
|
||||||
// RETURN_UV_ERR(UV_EFAULT)
|
|
||||||
//case EINVAL:
|
|
||||||
// RETURN_UV_ERR(UV_EINVAL);
|
|
||||||
//case ENOTTY:
|
|
||||||
// RETURN_UV_ERR(UV_ENOTTY);
|
|
||||||
}
|
|
||||||
ERR_PRINT("ioctl(2) failed");
|
|
||||||
return FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Foreground Process Name
|
|
||||||
*/
|
|
||||||
String PTYUnix::process(int p_fd, String p_tty) {
|
|
||||||
int fd = p_fd;
|
|
||||||
|
|
||||||
char *tty = strdup(p_tty.utf8().get_data());
|
|
||||||
char *name = pty_getproc(fd, tty);
|
|
||||||
std::free(tty);
|
|
||||||
|
|
||||||
if (name == NULL) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
String name_ = String(name);
|
|
||||||
std::free(name);
|
|
||||||
return name_;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* execvpe
|
|
||||||
*/
|
|
||||||
|
|
||||||
// execvpe(3) is not portable.
|
|
||||||
// http://www.gnu.org/software/gnulib/manual/html_node/execvpe.html
|
|
||||||
static int pty_execvpe(const char *file, char **argv, char **envp) {
|
|
||||||
char **old = environ;
|
|
||||||
environ = envp;
|
|
||||||
int ret = execvp(file, argv);
|
|
||||||
environ = old;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nonblocking FD
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int pty_nonblock(int fd) {
|
|
||||||
int flags = fcntl(fd, F_GETFL, 0);
|
|
||||||
if (flags == -1)
|
|
||||||
return -1;
|
|
||||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pty_waitpid
|
|
||||||
* Wait for SIGCHLD to read exit status.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static void pty_waitpid(void *data) {
|
|
||||||
int ret;
|
|
||||||
int stat_loc;
|
|
||||||
|
|
||||||
pty_baton *baton = static_cast<pty_baton *>(data);
|
|
||||||
|
|
||||||
errno = 0;
|
|
||||||
|
|
||||||
if ((ret = waitpid(baton->pid, &stat_loc, 0)) != baton->pid) {
|
|
||||||
if (ret == -1 && errno == EINTR) {
|
|
||||||
return pty_waitpid(baton);
|
|
||||||
}
|
|
||||||
if (ret == -1 && errno == ECHILD) {
|
|
||||||
// XXX node v0.8.x seems to have this problem.
|
|
||||||
// waitpid is already handled elsewhere.
|
|
||||||
;
|
|
||||||
} else {
|
|
||||||
// assert(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WIFEXITED(stat_loc)) {
|
|
||||||
baton->exit_code = WEXITSTATUS(stat_loc); // errno?
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WIFSIGNALED(stat_loc)) {
|
|
||||||
baton->signal_code = WTERMSIG(stat_loc);
|
|
||||||
}
|
|
||||||
|
|
||||||
uv_async_send(&baton->async);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pty_after_waitpid
|
|
||||||
* Callback after exit status has been read.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static void pty_after_waitpid(uv_async_t *async) {
|
|
||||||
pty_baton *baton = static_cast<pty_baton *>(async->data);
|
|
||||||
|
|
||||||
Array argv = Array::make(baton->exit_code, baton->signal_code);
|
|
||||||
|
|
||||||
if (baton->cb.is_valid()) {
|
|
||||||
baton->cb.callv(argv);
|
|
||||||
baton->cb = (Variant) nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
uv_close((uv_handle_t *)async, pty_after_close);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pty_after_close
|
|
||||||
* uv_close() callback - free handle data
|
|
||||||
*/
|
|
||||||
|
|
||||||
static void pty_after_close(uv_handle_t *handle) {
|
|
||||||
uv_async_t *async = (uv_async_t *)handle;
|
|
||||||
pty_baton *baton = static_cast<pty_baton *>(async->data);
|
|
||||||
delete baton;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pty_getproc
|
|
||||||
* Taken from tmux.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Taken from: tmux (http://tmux.sourceforge.net/)
|
|
||||||
// Copyright (c) 2009 Nicholas Marriott <nicm@users.sourceforge.net>
|
|
||||||
// Copyright (c) 2009 Joshua Elsasser <josh@elsasser.org>
|
|
||||||
// Copyright (c) 2009 Todd Carson <toc@daybefore.net>
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
// WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
|
|
||||||
// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
#if defined(__linux__)
|
|
||||||
|
|
||||||
static char *pty_getproc(int fd, char *tty) {
|
|
||||||
FILE *f;
|
|
||||||
char *path, *buf;
|
|
||||||
size_t len;
|
|
||||||
int ch;
|
|
||||||
pid_t pgrp;
|
|
||||||
int r;
|
|
||||||
|
|
||||||
if ((pgrp = tcgetpgrp(fd)) == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
r = asprintf(&path, "/proc/%lld/cmdline", (long long)pgrp);
|
|
||||||
if (r == -1 || path == NULL)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
if ((f = fopen(path, "r")) == NULL) {
|
|
||||||
free(path);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
free(path);
|
|
||||||
|
|
||||||
len = 0;
|
|
||||||
buf = NULL;
|
|
||||||
while ((ch = fgetc(f)) != EOF) {
|
|
||||||
if (ch == '\0')
|
|
||||||
break;
|
|
||||||
buf = (char *)realloc(buf, len + 2);
|
|
||||||
if (buf == NULL)
|
|
||||||
return NULL;
|
|
||||||
buf[len++] = ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buf != NULL) {
|
|
||||||
buf[len] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
|
|
||||||
static char *pty_getproc(int fd, char *tty) {
|
|
||||||
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, 0};
|
|
||||||
size_t size;
|
|
||||||
struct kinfo_proc kp;
|
|
||||||
|
|
||||||
if ((mib[3] = tcgetpgrp(fd)) == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
size = sizeof kp;
|
|
||||||
if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size != (sizeof kp) || *kp.kp_proc.p_comm == '\0') {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return strdup(kp.kp_proc.p_comm);
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
|
||||||
|
|
||||||
static char *pty_getproc(int fd, char *tty) { return NULL; }
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* openpty(3) / forkpty(3)
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int pty_openpty(int *amaster, int *aslave, char *name,
|
|
||||||
const struct termios *termp,
|
|
||||||
const struct winsize *winp) {
|
|
||||||
#if defined(__sun)
|
|
||||||
char *slave_name;
|
|
||||||
int slave;
|
|
||||||
int master = open("/dev/ptmx", O_RDWR | O_NOCTTY);
|
|
||||||
if (master == -1)
|
|
||||||
return -1;
|
|
||||||
if (amaster)
|
|
||||||
*amaster = master;
|
|
||||||
|
|
||||||
if (grantpt(master) == -1)
|
|
||||||
goto err;
|
|
||||||
if (unlockpt(master) == -1)
|
|
||||||
goto err;
|
|
||||||
|
|
||||||
slave_name = ptsname(master);
|
|
||||||
if (slave_name == NULL)
|
|
||||||
goto err;
|
|
||||||
if (name)
|
|
||||||
strcpy(name, slave_name);
|
|
||||||
|
|
||||||
slave = open(slave_name, O_RDWR | O_NOCTTY);
|
|
||||||
if (slave == -1)
|
|
||||||
goto err;
|
|
||||||
if (aslave)
|
|
||||||
*aslave = slave;
|
|
||||||
|
|
||||||
ioctl(slave, I_PUSH, "ptem");
|
|
||||||
ioctl(slave, I_PUSH, "ldterm");
|
|
||||||
ioctl(slave, I_PUSH, "ttcompat");
|
|
||||||
|
|
||||||
if (termp)
|
|
||||||
tcsetattr(slave, TCSAFLUSH, termp);
|
|
||||||
if (winp)
|
|
||||||
ioctl(slave, TIOCSWINSZ, winp);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
err:
|
|
||||||
close(master);
|
|
||||||
return -1;
|
|
||||||
#else
|
|
||||||
return openpty(amaster, aslave, name, (termios *)termp, (winsize *)winp);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
static pid_t pty_forkpty(int *amaster, char *name, const struct termios *termp,
|
|
||||||
const struct winsize *winp) {
|
|
||||||
#if defined(__sun)
|
|
||||||
int master, slave;
|
|
||||||
|
|
||||||
int ret = pty_openpty(&master, &slave, name, termp, winp);
|
|
||||||
if (ret == -1)
|
|
||||||
return -1;
|
|
||||||
if (amaster)
|
|
||||||
*amaster = master;
|
|
||||||
|
|
||||||
pid_t pid = fork();
|
|
||||||
|
|
||||||
switch (pid) {
|
|
||||||
case -1: // error in fork, we are still in parent
|
|
||||||
close(master);
|
|
||||||
close(slave);
|
|
||||||
return -1;
|
|
||||||
case 0: // we are in the child process
|
|
||||||
close(master);
|
|
||||||
setsid();
|
|
||||||
|
|
||||||
#if defined(TIOCSCTTY)
|
|
||||||
// glibc does this
|
|
||||||
if (ioctl(slave, TIOCSCTTY, NULL) == -1) {
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
dup2(slave, 0);
|
|
||||||
dup2(slave, 1);
|
|
||||||
dup2(slave, 2);
|
|
||||||
|
|
||||||
if (slave > 2)
|
|
||||||
close(slave);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
default: // we are in the parent process
|
|
||||||
close(slave);
|
|
||||||
return pid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
#else
|
|
||||||
return forkpty(amaster, name, (termios *)termp, (winsize *)winp);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init
|
|
||||||
*/
|
|
||||||
|
|
||||||
void PTYUnix::_bind_methods() {
|
|
||||||
ClassDB::bind_method(D_METHOD("_init"), &PTYUnix::_init);
|
|
||||||
ClassDB::bind_method(D_METHOD("fork"), &PTYUnix::fork);
|
|
||||||
ClassDB::bind_method(D_METHOD("open"), &PTYUnix::open);
|
|
||||||
ClassDB::bind_method(D_METHOD("resize"), &PTYUnix::resize);
|
|
||||||
ClassDB::bind_method(D_METHOD("process"), &PTYUnix::process);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PTYUnix::_init() {}
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -1,31 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021-2022, 2024 Leroy Hopson <godot-xterm@leroy.nix.nz>
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <godot_cpp/classes/ref_counted.hpp>
|
|
||||||
#include <godot_cpp/variant/callable.hpp>
|
|
||||||
|
|
||||||
namespace godot {
|
|
||||||
|
|
||||||
class PTYUnix : public RefCounted {
|
|
||||||
GDCLASS(PTYUnix, RefCounted)
|
|
||||||
|
|
||||||
public:
|
|
||||||
Array fork(String file,
|
|
||||||
int _ignored, /* FIXME: For some reason Pipe throws
|
|
||||||
ENOTSOCK in read callback if args (or another non-empty,
|
|
||||||
non-zero) value is in this position. */
|
|
||||||
PackedStringArray args, PackedStringArray env, String cwd, int cols,
|
|
||||||
int rows, int uid, int gid, bool utf8, Callable on_exit);
|
|
||||||
Array open(int cols, int rows);
|
|
||||||
Error resize(int fd, int cols, int rows);
|
|
||||||
String process(int fd, String tty);
|
|
||||||
|
|
||||||
void _init();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
static void _bind_methods();
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace godot
|
|
|
@ -1,827 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
|
||||||
* Copyright (c) 2017, Daniel Imms (MIT License)
|
|
||||||
* Copyright (c) 2024, Leroy Hopson (MIT License)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*
|
|
||||||
* pty.cc:
|
|
||||||
* This file is responsible for starting processes
|
|
||||||
* with pseudo-terminal file descriptors.
|
|
||||||
*
|
|
||||||
* See:
|
|
||||||
* man pty
|
|
||||||
* man tty_ioctl
|
|
||||||
* man termios
|
|
||||||
* man forkpty
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Includes
|
|
||||||
*/
|
|
||||||
|
|
||||||
using namespace godot;
|
|
||||||
|
|
||||||
#include <godot_cpp/variant/variant.hpp>
|
|
||||||
#include <uv.h>
|
|
||||||
|
|
||||||
#include <errno.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <sys/ioctl.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <signal.h>
|
|
||||||
|
|
||||||
/* forkpty */
|
|
||||||
/* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */
|
|
||||||
#if defined(__linux__)
|
|
||||||
#include <pty.h>
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
#include <util.h>
|
|
||||||
#elif defined(__FreeBSD__)
|
|
||||||
#include <libutil.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Some platforms name VWERASE and VDISCARD differently */
|
|
||||||
#if !defined(VWERASE) && defined(VWERSE)
|
|
||||||
#define VWERASE VWERSE
|
|
||||||
#endif
|
|
||||||
#if !defined(VDISCARD) && defined(VDISCRD)
|
|
||||||
#define VDISCARD VDISCRD
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* for pty_getproc */
|
|
||||||
#if defined(__linux__)
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
#include <libproc.h>
|
|
||||||
#include <os/availability.h>
|
|
||||||
#include <paths.h>
|
|
||||||
#include <spawn.h>
|
|
||||||
#include <sys/event.h>
|
|
||||||
#include <termios.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* NSIG - macro for highest signal + 1, should be defined */
|
|
||||||
#ifndef NSIG
|
|
||||||
#define NSIG 32
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* macOS 10.14 back does not define this constant */
|
|
||||||
#ifndef POSIX_SPAWN_SETSID
|
|
||||||
#define POSIX_SPAWN_SETSID 1024
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* environ for execvpe */
|
|
||||||
/* node/src/node_child_process.cc */
|
|
||||||
#if !defined(__APPLE__)
|
|
||||||
extern char **environ;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
extern "C" {
|
|
||||||
// Changes the current thread's directory to a path or directory file
|
|
||||||
// descriptor. libpthread only exposes a syscall wrapper starting in
|
|
||||||
// macOS 10.12, but the system call dates back to macOS 10.5. On older OSes,
|
|
||||||
// the syscall is issued directly.
|
|
||||||
int pthread_chdir_np(const char* dir) API_AVAILABLE(macosx(10.12));
|
|
||||||
int pthread_fchdir_np(int fd) API_AVAILABLE(macosx(10.12));
|
|
||||||
}
|
|
||||||
|
|
||||||
#define HANDLE_EINTR(x) ({ \
|
|
||||||
int eintr_wrapper_counter = 0; \
|
|
||||||
decltype(x) eintr_wrapper_result; \
|
|
||||||
do { \
|
|
||||||
eintr_wrapper_result = (x); \
|
|
||||||
} while (eintr_wrapper_result == -1 && errno == EINTR && \
|
|
||||||
eintr_wrapper_counter++ < 100); \
|
|
||||||
eintr_wrapper_result; \
|
|
||||||
})
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Structs
|
|
||||||
*/
|
|
||||||
|
|
||||||
struct pty_baton {
|
|
||||||
Nan::Persistent<v8::Function> cb;
|
|
||||||
int exit_code;
|
|
||||||
int signal_code;
|
|
||||||
pid_t pid;
|
|
||||||
uv_async_t async;
|
|
||||||
uv_thread_t tid;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
NAN_METHOD(PtyFork);
|
|
||||||
NAN_METHOD(PtyOpen);
|
|
||||||
NAN_METHOD(PtyResize);
|
|
||||||
NAN_METHOD(PtyGetProc);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int
|
|
||||||
pty_nonblock(int);
|
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
static char *
|
|
||||||
pty_getproc(int);
|
|
||||||
#else
|
|
||||||
static char *
|
|
||||||
pty_getproc(int, char *);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static void
|
|
||||||
pty_waitpid(void *);
|
|
||||||
|
|
||||||
static void
|
|
||||||
pty_after_waitpid(uv_async_t *);
|
|
||||||
|
|
||||||
static void
|
|
||||||
pty_after_close(uv_handle_t *);
|
|
||||||
|
|
||||||
#if defined(__APPLE__) || defined(__OpenBSD__)
|
|
||||||
static void
|
|
||||||
pty_posix_spawn(char** argv, char** env,
|
|
||||||
const struct termios *termp,
|
|
||||||
const struct winsize *winp,
|
|
||||||
int* master,
|
|
||||||
pid_t* pid,
|
|
||||||
int* err);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Dictionary fork(
|
|
||||||
const String &p_file,
|
|
||||||
const PackedStringArray &p_args,
|
|
||||||
const PackedStringArray &p_env,
|
|
||||||
const String &p_cwd,
|
|
||||||
const int p_cols,
|
|
||||||
const int p_rows,
|
|
||||||
const int p_uid,
|
|
||||||
const int p_gid,
|
|
||||||
const bool p_utf8,
|
|
||||||
const String &p_helper_path,
|
|
||||||
const Callable &p_on_exit
|
|
||||||
) {
|
|
||||||
// file
|
|
||||||
std::string file = p_file.utf8().get_data();
|
|
||||||
|
|
||||||
// args
|
|
||||||
v8::Local<v8::Array> argv_ = v8::Local<v8::Array>::Cast(info[1]);
|
|
||||||
|
|
||||||
// env
|
|
||||||
v8::Local<v8::Array> env_ = v8::Local<v8::Array>::Cast(info[2]);
|
|
||||||
int envc = env_->Length();
|
|
||||||
char **env = new char*[envc+1];
|
|
||||||
env[envc] = NULL;
|
|
||||||
for (int i = 0; i < envc; i++) {
|
|
||||||
Nan::Utf8String pair(Nan::Get(env_, i).ToLocalChecked());
|
|
||||||
env[i] = strdup(*pair);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cwd
|
|
||||||
Nan::Utf8String cwd_(info[3]);
|
|
||||||
|
|
||||||
// size
|
|
||||||
struct winsize winp;
|
|
||||||
winp.ws_col = info[4]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
winp.ws_row = info[5]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
winp.ws_xpixel = 0;
|
|
||||||
winp.ws_ypixel = 0;
|
|
||||||
|
|
||||||
#if !defined(__APPLE__)
|
|
||||||
// uid / gid
|
|
||||||
int uid = info[6]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
int gid = info[7]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// termios
|
|
||||||
struct termios t = termios();
|
|
||||||
struct termios *term = &t;
|
|
||||||
term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT;
|
|
||||||
if (Nan::To<bool>(info[8]).FromJust()) {
|
|
||||||
#if defined(IUTF8)
|
|
||||||
term->c_iflag |= IUTF8;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
term->c_oflag = OPOST | ONLCR;
|
|
||||||
term->c_cflag = CREAD | CS8 | HUPCL;
|
|
||||||
term->c_lflag = ICANON | ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOKE | ECHOCTL;
|
|
||||||
|
|
||||||
term->c_cc[VEOF] = 4;
|
|
||||||
term->c_cc[VEOL] = -1;
|
|
||||||
term->c_cc[VEOL2] = -1;
|
|
||||||
term->c_cc[VERASE] = 0x7f;
|
|
||||||
term->c_cc[VWERASE] = 23;
|
|
||||||
term->c_cc[VKILL] = 21;
|
|
||||||
term->c_cc[VREPRINT] = 18;
|
|
||||||
term->c_cc[VINTR] = 3;
|
|
||||||
term->c_cc[VQUIT] = 0x1c;
|
|
||||||
term->c_cc[VSUSP] = 26;
|
|
||||||
term->c_cc[VSTART] = 17;
|
|
||||||
term->c_cc[VSTOP] = 19;
|
|
||||||
term->c_cc[VLNEXT] = 22;
|
|
||||||
term->c_cc[VDISCARD] = 15;
|
|
||||||
term->c_cc[VMIN] = 1;
|
|
||||||
term->c_cc[VTIME] = 0;
|
|
||||||
|
|
||||||
#if (__APPLE__)
|
|
||||||
term->c_cc[VDSUSP] = 25;
|
|
||||||
term->c_cc[VSTATUS] = 20;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
cfsetispeed(term, B38400);
|
|
||||||
cfsetospeed(term, B38400);
|
|
||||||
|
|
||||||
// helperPath
|
|
||||||
Nan::Utf8String helper_path(info[9]);
|
|
||||||
|
|
||||||
pid_t pid;
|
|
||||||
int master;
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
int argc = argv_->Length();
|
|
||||||
int argl = argc + 4;
|
|
||||||
char **argv = new char*[argl];
|
|
||||||
argv[0] = strdup(*helper_path);
|
|
||||||
argv[1] = strdup(*cwd_);
|
|
||||||
argv[2] = strdup(*file);
|
|
||||||
argv[argl - 1] = NULL;
|
|
||||||
for (int i = 0; i < argc; i++) {
|
|
||||||
Nan::Utf8String arg(Nan::Get(argv_, i).ToLocalChecked());
|
|
||||||
argv[i + 3] = strdup(*arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
int err = -1;
|
|
||||||
pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err);
|
|
||||||
if (err != 0) {
|
|
||||||
Nan::ThrowError("posix_spawnp failed.");
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
if (pty_nonblock(master) == -1) {
|
|
||||||
Nan::ThrowError("Could not set master fd to nonblocking.");
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
int argc = argv_->Length();
|
|
||||||
int argl = argc + 2;
|
|
||||||
char **argv = new char*[argl];
|
|
||||||
argv[0] = strdup(*file);
|
|
||||||
argv[argl - 1] = NULL;
|
|
||||||
for (int i = 0; i < argc; i++) {
|
|
||||||
Nan::Utf8String arg(Nan::Get(argv_, i).ToLocalChecked());
|
|
||||||
argv[i + 1] = strdup(*arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
char* cwd = strdup(*cwd_);
|
|
||||||
sigset_t newmask, oldmask;
|
|
||||||
struct sigaction sig_action;
|
|
||||||
// temporarily block all signals
|
|
||||||
// this is needed due to a race condition in openpty
|
|
||||||
// and to avoid running signal handlers in the child
|
|
||||||
// before exec* happened
|
|
||||||
sigfillset(&newmask);
|
|
||||||
pthread_sigmask(SIG_SETMASK, &newmask, &oldmask);
|
|
||||||
|
|
||||||
pid = forkpty(&master, nullptr, static_cast<termios*>(term), static_cast<winsize*>(&winp));
|
|
||||||
|
|
||||||
if (!pid) {
|
|
||||||
// remove all signal handler from child
|
|
||||||
sig_action.sa_handler = SIG_DFL;
|
|
||||||
sig_action.sa_flags = 0;
|
|
||||||
sigemptyset(&sig_action.sa_mask);
|
|
||||||
for (int i = 0 ; i < NSIG ; i++) { // NSIG is a macro for all signals + 1
|
|
||||||
sigaction(i, &sig_action, NULL);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < argl; i++) free(argv[i]);
|
|
||||||
delete[] argv;
|
|
||||||
|
|
||||||
for (int i = 0; i < envc; i++) free(env[i]);
|
|
||||||
delete[] env;
|
|
||||||
|
|
||||||
free(cwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-enable signals
|
|
||||||
pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
|
|
||||||
|
|
||||||
switch (pid) {
|
|
||||||
case -1:
|
|
||||||
Nan::ThrowError("forkpty(3) failed.");
|
|
||||||
goto done;
|
|
||||||
case 0:
|
|
||||||
if (strlen(cwd)) {
|
|
||||||
if (chdir(cwd) == -1) {
|
|
||||||
perror("chdir(2) failed.");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uid != -1 && gid != -1) {
|
|
||||||
if (setgid(gid) == -1) {
|
|
||||||
perror("setgid(2) failed.");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
if (setuid(uid) == -1) {
|
|
||||||
perror("setuid(2) failed.");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
char **old = environ;
|
|
||||||
environ = env;
|
|
||||||
execvp(argv[0], argv);
|
|
||||||
environ = old;
|
|
||||||
perror("execvp(3) failed.");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if (pty_nonblock(master) == -1) {
|
|
||||||
Nan::ThrowError("Could not set master fd to nonblocking.");
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
{
|
|
||||||
v8::Local<v8::Object> obj = Nan::New<v8::Object>();
|
|
||||||
Nan::Set(obj,
|
|
||||||
Nan::New<v8::String>("fd").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>(master));
|
|
||||||
Nan::Set(obj,
|
|
||||||
Nan::New<v8::String>("pid").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>(pid));
|
|
||||||
Nan::Set(obj,
|
|
||||||
Nan::New<v8::String>("pty").ToLocalChecked(),
|
|
||||||
Nan::New<v8::String>(ptsname(master)).ToLocalChecked());
|
|
||||||
|
|
||||||
pty_baton *baton = new pty_baton();
|
|
||||||
baton->exit_code = 0;
|
|
||||||
baton->signal_code = 0;
|
|
||||||
baton->cb.Reset(v8::Local<v8::Function>::Cast(info[10]));
|
|
||||||
baton->pid = pid;
|
|
||||||
baton->async.data = baton;
|
|
||||||
|
|
||||||
uv_async_init(uv_default_loop(), &baton->async, pty_after_waitpid);
|
|
||||||
|
|
||||||
uv_thread_create(&baton->tid, pty_waitpid, static_cast<void*>(baton));
|
|
||||||
|
|
||||||
return info.GetReturnValue().Set(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
done:
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
for (int i = 0; i < argl; i++) free(argv[i]);
|
|
||||||
delete[] argv;
|
|
||||||
|
|
||||||
for (int i = 0; i < envc; i++) free(env[i]);
|
|
||||||
delete[] env;
|
|
||||||
#endif
|
|
||||||
return info.GetReturnValue().SetUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
NAN_METHOD(PtyOpen) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
|
|
||||||
if (info.Length() != 2 ||
|
|
||||||
!info[0]->IsNumber() ||
|
|
||||||
!info[1]->IsNumber()) {
|
|
||||||
return Nan::ThrowError("Usage: pty.open(cols, rows)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// size
|
|
||||||
struct winsize winp;
|
|
||||||
winp.ws_col = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
winp.ws_row = info[1]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
winp.ws_xpixel = 0;
|
|
||||||
winp.ws_ypixel = 0;
|
|
||||||
|
|
||||||
// pty
|
|
||||||
int master, slave;
|
|
||||||
int ret = openpty(&master, &slave, nullptr, NULL, static_cast<winsize*>(&winp));
|
|
||||||
|
|
||||||
if (ret == -1) {
|
|
||||||
return Nan::ThrowError("openpty(3) failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pty_nonblock(master) == -1) {
|
|
||||||
return Nan::ThrowError("Could not set master fd to nonblocking.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pty_nonblock(slave) == -1) {
|
|
||||||
return Nan::ThrowError("Could not set slave fd to nonblocking.");
|
|
||||||
}
|
|
||||||
|
|
||||||
v8::Local<v8::Object> obj = Nan::New<v8::Object>();
|
|
||||||
Nan::Set(obj,
|
|
||||||
Nan::New<v8::String>("master").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>(master));
|
|
||||||
Nan::Set(obj,
|
|
||||||
Nan::New<v8::String>("slave").ToLocalChecked(),
|
|
||||||
Nan::New<v8::Number>(slave));
|
|
||||||
Nan::Set(obj,
|
|
||||||
Nan::New<v8::String>("pty").ToLocalChecked(),
|
|
||||||
Nan::New<v8::String>(ptsname(master)).ToLocalChecked());
|
|
||||||
|
|
||||||
return info.GetReturnValue().Set(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
NAN_METHOD(PtyResize) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
|
|
||||||
if (info.Length() != 3 ||
|
|
||||||
!info[0]->IsNumber() ||
|
|
||||||
!info[1]->IsNumber() ||
|
|
||||||
!info[2]->IsNumber()) {
|
|
||||||
return Nan::ThrowError("Usage: pty.resize(fd, cols, rows)");
|
|
||||||
}
|
|
||||||
|
|
||||||
int fd = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
|
|
||||||
struct winsize winp;
|
|
||||||
winp.ws_col = info[1]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
winp.ws_row = info[2]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
winp.ws_xpixel = 0;
|
|
||||||
winp.ws_ypixel = 0;
|
|
||||||
|
|
||||||
if (ioctl(fd, TIOCSWINSZ, &winp) == -1) {
|
|
||||||
switch (errno) {
|
|
||||||
case EBADF: return Nan::ThrowError("ioctl(2) failed, EBADF");
|
|
||||||
case EFAULT: return Nan::ThrowError("ioctl(2) failed, EFAULT");
|
|
||||||
case EINVAL: return Nan::ThrowError("ioctl(2) failed, EINVAL");
|
|
||||||
case ENOTTY: return Nan::ThrowError("ioctl(2) failed, ENOTTY");
|
|
||||||
}
|
|
||||||
return Nan::ThrowError("ioctl(2) failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
return info.GetReturnValue().SetUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Foreground Process Name
|
|
||||||
*/
|
|
||||||
NAN_METHOD(PtyGetProc) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
if (info.Length() != 1 ||
|
|
||||||
!info[0]->IsNumber()) {
|
|
||||||
return Nan::ThrowError("Usage: pty.process(pid)");
|
|
||||||
}
|
|
||||||
|
|
||||||
int pid = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
char *name = pty_getproc(pid);
|
|
||||||
#else
|
|
||||||
if (info.Length() != 2 ||
|
|
||||||
!info[0]->IsNumber() ||
|
|
||||||
!info[1]->IsString()) {
|
|
||||||
return Nan::ThrowError("Usage: pty.process(fd, tty)");
|
|
||||||
}
|
|
||||||
|
|
||||||
int fd = info[0]->IntegerValue(Nan::GetCurrentContext()).FromJust();
|
|
||||||
|
|
||||||
Nan::Utf8String tty_(info[1]);
|
|
||||||
char *tty = strdup(*tty_);
|
|
||||||
char *name = pty_getproc(fd, tty);
|
|
||||||
free(tty);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (name == NULL) {
|
|
||||||
return info.GetReturnValue().SetUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
v8::Local<v8::String> name_ = Nan::New<v8::String>(name).ToLocalChecked();
|
|
||||||
free(name);
|
|
||||||
return info.GetReturnValue().Set(name_);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nonblocking FD
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int
|
|
||||||
pty_nonblock(int fd) {
|
|
||||||
int flags = fcntl(fd, F_GETFL, 0);
|
|
||||||
if (flags == -1) return -1;
|
|
||||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pty_waitpid
|
|
||||||
* Wait for SIGCHLD to read exit status.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static void
|
|
||||||
pty_waitpid(void *data) {
|
|
||||||
int ret;
|
|
||||||
int stat_loc;
|
|
||||||
pty_baton *baton = static_cast<pty_baton*>(data);
|
|
||||||
errno = 0;
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
// Based on
|
|
||||||
// https://source.chromium.org/chromium/chromium/src/+/main:base/process/kill_mac.cc;l=35-69?
|
|
||||||
int kq = HANDLE_EINTR(kqueue());
|
|
||||||
struct kevent change = {0};
|
|
||||||
EV_SET(&change, baton->pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL);
|
|
||||||
ret = HANDLE_EINTR(kevent(kq, &change, 1, NULL, 0, NULL));
|
|
||||||
if (ret == -1) {
|
|
||||||
if (errno == ESRCH) {
|
|
||||||
// At this point, one of the following has occurred:
|
|
||||||
// 1. The process has died but has not yet been reaped.
|
|
||||||
// 2. The process has died and has already been reaped.
|
|
||||||
// 3. The process is in the process of dying. It's no longer
|
|
||||||
// kqueueable, but it may not be waitable yet either. Mark calls
|
|
||||||
// this case the "zombie death race".
|
|
||||||
ret = HANDLE_EINTR(waitpid(baton->pid, &stat_loc, WNOHANG));
|
|
||||||
if (ret == 0) {
|
|
||||||
ret = kill(baton->pid, SIGKILL);
|
|
||||||
if (ret != -1) {
|
|
||||||
HANDLE_EINTR(waitpid(baton->pid, &stat_loc, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
struct kevent event = {0};
|
|
||||||
ret = HANDLE_EINTR(kevent(kq, NULL, 0, &event, 1, NULL));
|
|
||||||
if (ret == 1) {
|
|
||||||
if ((event.fflags & NOTE_EXIT) &&
|
|
||||||
(event.ident == static_cast<uintptr_t>(baton->pid))) {
|
|
||||||
// The process is dead or dying. This won't block for long, if at
|
|
||||||
// all.
|
|
||||||
HANDLE_EINTR(waitpid(baton->pid, &stat_loc, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
if ((ret = waitpid(baton->pid, &stat_loc, 0)) != baton->pid) {
|
|
||||||
if (ret == -1 && errno == EINTR) {
|
|
||||||
return pty_waitpid(baton);
|
|
||||||
}
|
|
||||||
if (ret == -1 && errno == ECHILD) {
|
|
||||||
// XXX node v0.8.x seems to have this problem.
|
|
||||||
// waitpid is already handled elsewhere.
|
|
||||||
;
|
|
||||||
} else {
|
|
||||||
assert(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (WIFEXITED(stat_loc)) {
|
|
||||||
baton->exit_code = WEXITSTATUS(stat_loc); // errno?
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WIFSIGNALED(stat_loc)) {
|
|
||||||
baton->signal_code = WTERMSIG(stat_loc);
|
|
||||||
}
|
|
||||||
|
|
||||||
uv_async_send(&baton->async);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pty_after_waitpid
|
|
||||||
* Callback after exit status has been read.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static void
|
|
||||||
pty_after_waitpid(uv_async_t *async) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
pty_baton *baton = static_cast<pty_baton*>(async->data);
|
|
||||||
|
|
||||||
v8::Local<v8::Value> argv[] = {
|
|
||||||
Nan::New<v8::Integer>(baton->exit_code),
|
|
||||||
Nan::New<v8::Integer>(baton->signal_code),
|
|
||||||
};
|
|
||||||
|
|
||||||
v8::Local<v8::Function> cb = Nan::New<v8::Function>(baton->cb);
|
|
||||||
baton->cb.Reset();
|
|
||||||
memset(&baton->cb, -1, sizeof(baton->cb));
|
|
||||||
Nan::AsyncResource resource("pty_after_waitpid");
|
|
||||||
resource.runInAsyncScope(Nan::GetCurrentContext()->Global(), cb, 2, argv);
|
|
||||||
|
|
||||||
uv_close((uv_handle_t *)async, pty_after_close);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pty_after_close
|
|
||||||
* uv_close() callback - free handle data
|
|
||||||
*/
|
|
||||||
|
|
||||||
static void
|
|
||||||
pty_after_close(uv_handle_t *handle) {
|
|
||||||
uv_async_t *async = (uv_async_t *)handle;
|
|
||||||
pty_baton *baton = static_cast<pty_baton*>(async->data);
|
|
||||||
delete baton;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pty_getproc
|
|
||||||
* Taken from tmux.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Taken from: tmux (http://tmux.sourceforge.net/)
|
|
||||||
// Copyright (c) 2009 Nicholas Marriott <nicm@users.sourceforge.net>
|
|
||||||
// Copyright (c) 2009 Joshua Elsasser <josh@elsasser.org>
|
|
||||||
// Copyright (c) 2009 Todd Carson <toc@daybefore.net>
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
// WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
|
|
||||||
// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
#if defined(__linux__)
|
|
||||||
|
|
||||||
static char *
|
|
||||||
pty_getproc(int fd, char *tty) {
|
|
||||||
FILE *f;
|
|
||||||
char *path, *buf;
|
|
||||||
size_t len;
|
|
||||||
int ch;
|
|
||||||
pid_t pgrp;
|
|
||||||
int r;
|
|
||||||
|
|
||||||
if ((pgrp = tcgetpgrp(fd)) == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
r = asprintf(&path, "/proc/%lld/cmdline", (long long)pgrp);
|
|
||||||
if (r == -1 || path == NULL) return NULL;
|
|
||||||
|
|
||||||
if ((f = fopen(path, "r")) == NULL) {
|
|
||||||
free(path);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
free(path);
|
|
||||||
|
|
||||||
len = 0;
|
|
||||||
buf = NULL;
|
|
||||||
while ((ch = fgetc(f)) != EOF) {
|
|
||||||
if (ch == '\0') break;
|
|
||||||
buf = (char *)realloc(buf, len + 2);
|
|
||||||
if (buf == NULL) return NULL;
|
|
||||||
buf[len++] = ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buf != NULL) {
|
|
||||||
buf[len] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
|
|
||||||
static char *
|
|
||||||
pty_getproc(int pid) {
|
|
||||||
char pname[MAXCOMLEN + 1];
|
|
||||||
if (!proc_name(pid, pname, sizeof(pname))) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return strdup(pname);
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
|
||||||
|
|
||||||
static char *
|
|
||||||
pty_getproc(int fd, char *tty) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
static void
|
|
||||||
pty_posix_spawn(char** argv, char** env,
|
|
||||||
const struct termios *termp,
|
|
||||||
const struct winsize *winp,
|
|
||||||
int* master,
|
|
||||||
pid_t* pid,
|
|
||||||
int* err) {
|
|
||||||
int low_fds[3];
|
|
||||||
size_t count = 0;
|
|
||||||
|
|
||||||
for (; count < 3; count++) {
|
|
||||||
low_fds[count] = posix_openpt(O_RDWR);
|
|
||||||
if (low_fds[count] >= STDERR_FILENO)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
int flags = POSIX_SPAWN_CLOEXEC_DEFAULT |
|
|
||||||
POSIX_SPAWN_SETSIGDEF |
|
|
||||||
POSIX_SPAWN_SETSIGMASK |
|
|
||||||
POSIX_SPAWN_SETSID;
|
|
||||||
*master = posix_openpt(O_RDWR);
|
|
||||||
if (*master == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int res = grantpt(*master) || unlockpt(*master);
|
|
||||||
if (res == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use TIOCPTYGNAME instead of ptsname() to avoid threading problems.
|
|
||||||
int slave;
|
|
||||||
char slave_pty_name[128];
|
|
||||||
res = ioctl(*master, TIOCPTYGNAME, slave_pty_name);
|
|
||||||
if (res == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
slave = open(slave_pty_name, O_RDWR | O_NOCTTY);
|
|
||||||
if (slave == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (termp) {
|
|
||||||
res = tcsetattr(slave, TCSANOW, termp);
|
|
||||||
if (res == -1) {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (winp) {
|
|
||||||
res = ioctl(slave, TIOCSWINSZ, winp);
|
|
||||||
if (res == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
posix_spawn_file_actions_t acts;
|
|
||||||
posix_spawn_file_actions_init(&acts);
|
|
||||||
posix_spawn_file_actions_adddup2(&acts, slave, STDIN_FILENO);
|
|
||||||
posix_spawn_file_actions_adddup2(&acts, slave, STDOUT_FILENO);
|
|
||||||
posix_spawn_file_actions_adddup2(&acts, slave, STDERR_FILENO);
|
|
||||||
posix_spawn_file_actions_addclose(&acts, slave);
|
|
||||||
posix_spawn_file_actions_addclose(&acts, *master);
|
|
||||||
|
|
||||||
posix_spawnattr_t attrs;
|
|
||||||
posix_spawnattr_init(&attrs);
|
|
||||||
*err = posix_spawnattr_setflags(&attrs, flags);
|
|
||||||
if (*err != 0) {
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
|
|
||||||
sigset_t signal_set;
|
|
||||||
/* Reset all signal the child to their default behavior */
|
|
||||||
sigfillset(&signal_set);
|
|
||||||
*err = posix_spawnattr_setsigdefault(&attrs, &signal_set);
|
|
||||||
if (*err != 0) {
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset the signal mask for all signals */
|
|
||||||
sigemptyset(&signal_set);
|
|
||||||
*err = posix_spawnattr_setsigmask(&attrs, &signal_set);
|
|
||||||
if (*err != 0) {
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
|
|
||||||
do
|
|
||||||
*err = posix_spawn(pid, argv[0], &acts, &attrs, argv, env);
|
|
||||||
while (*err == EINTR);
|
|
||||||
done:
|
|
||||||
posix_spawn_file_actions_destroy(&acts);
|
|
||||||
posix_spawnattr_destroy(&attrs);
|
|
||||||
|
|
||||||
for (; count > 0; count--) {
|
|
||||||
close(low_fds[count]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init
|
|
||||||
*/
|
|
||||||
|
|
||||||
NAN_MODULE_INIT(init) {
|
|
||||||
Nan::HandleScope scope;
|
|
||||||
Nan::Export(target, "fork", PtyFork);
|
|
||||||
Nan::Export(target, "open", PtyOpen);
|
|
||||||
Nan::Export(target, "resize", PtyResize);
|
|
||||||
Nan::Export(target, "process", PtyGetProc);
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_MODULE(pty, init)
|
|
|
@ -1,788 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
|
||||||
* Copyright (c) 2017, Daniel Imms (MIT License)
|
|
||||||
* Copyright (c) 2024, Leroy Hopson (MIT License)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*
|
|
||||||
* pty.cc:
|
|
||||||
* This file is responsible for starting processes
|
|
||||||
* with pseudo-terminal file descriptors.
|
|
||||||
*
|
|
||||||
* See:
|
|
||||||
* man pty
|
|
||||||
* man tty_ioctl
|
|
||||||
* man termios
|
|
||||||
* man forkpty
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Includes
|
|
||||||
*/
|
|
||||||
|
|
||||||
#if !defined(_WIN32) && !defined(_PTY_DISABLED)
|
|
||||||
|
|
||||||
#define NODE_ADDON_API_DISABLE_DEPRECATED
|
|
||||||
#include <assert.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <sys/ioctl.h>
|
|
||||||
#include <sys/wait.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <signal.h>
|
|
||||||
|
|
||||||
/* forkpty */
|
|
||||||
/* http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html */
|
|
||||||
#if defined(__linux__)
|
|
||||||
#include <pty.h>
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
#include <util.h>
|
|
||||||
#elif defined(__FreeBSD__)
|
|
||||||
#include <libutil.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* Some platforms name VWERASE and VDISCARD differently */
|
|
||||||
#if !defined(VWERASE) && defined(VWERSE)
|
|
||||||
#define VWERASE VWERSE
|
|
||||||
#endif
|
|
||||||
#if !defined(VDISCARD) && defined(VDISCRD)
|
|
||||||
#define VDISCARD VDISCRD
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* for pty_getproc */
|
|
||||||
#if defined(__linux__)
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
#include <libproc.h>
|
|
||||||
#include <os/availability.h>
|
|
||||||
#include <paths.h>
|
|
||||||
#include <spawn.h>
|
|
||||||
#include <sys/event.h>
|
|
||||||
#include <sys/sysctl.h>
|
|
||||||
#include <termios.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* NSIG - macro for highest signal + 1, should be defined */
|
|
||||||
#ifndef NSIG
|
|
||||||
#define NSIG 32
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* macOS 10.14 back does not define this constant */
|
|
||||||
#ifndef POSIX_SPAWN_SETSID
|
|
||||||
#define POSIX_SPAWN_SETSID 1024
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* environ for execvpe */
|
|
||||||
/* node/src/node_child_process.cc */
|
|
||||||
#if !defined(__APPLE__)
|
|
||||||
extern char **environ;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
extern "C" {
|
|
||||||
// Changes the current thread's directory to a path or directory file
|
|
||||||
// descriptor. libpthread only exposes a syscall wrapper starting in
|
|
||||||
// macOS 10.12, but the system call dates back to macOS 10.5. On older OSes,
|
|
||||||
// the syscall is issued directly.
|
|
||||||
int pthread_chdir_np(const char* dir) API_AVAILABLE(macosx(10.12));
|
|
||||||
int pthread_fchdir_np(int fd) API_AVAILABLE(macosx(10.12));
|
|
||||||
}
|
|
||||||
|
|
||||||
#define HANDLE_EINTR(x) ({ \
|
|
||||||
int eintr_wrapper_counter = 0; \
|
|
||||||
decltype(x) eintr_wrapper_result; \
|
|
||||||
do { \
|
|
||||||
eintr_wrapper_result = (x); \
|
|
||||||
} while (eintr_wrapper_result == -1 && errno == EINTR && \
|
|
||||||
eintr_wrapper_counter++ < 100); \
|
|
||||||
eintr_wrapper_result; \
|
|
||||||
})
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct ExitEvent {
|
|
||||||
int exit_code = 0, signal_code = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) {
|
|
||||||
std::thread *th = new std::thread;
|
|
||||||
// Don't use Napi::AsyncWorker which is limited by UV_THREADPOOL_SIZE.
|
|
||||||
auto tsfn = Napi::ThreadSafeFunction::New(
|
|
||||||
env,
|
|
||||||
cb, // JavaScript function called asynchronously
|
|
||||||
"SetupExitCallback_resource", // Name
|
|
||||||
0, // Unlimited queue
|
|
||||||
1, // Only one thread will use this initially
|
|
||||||
[th](Napi::Env) { // Finalizer used to clean threads up
|
|
||||||
th->join();
|
|
||||||
delete th;
|
|
||||||
});
|
|
||||||
*th = std::thread([tsfn = std::move(tsfn), pid] {
|
|
||||||
auto callback = [](Napi::Env env, Napi::Function cb, ExitEvent *exit_event) {
|
|
||||||
cb.Call({Napi::Number::New(env, exit_event->exit_code),
|
|
||||||
Napi::Number::New(env, exit_event->signal_code)});
|
|
||||||
delete exit_event;
|
|
||||||
};
|
|
||||||
|
|
||||||
int ret;
|
|
||||||
int stat_loc;
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
// Based on
|
|
||||||
// https://source.chromium.org/chromium/chromium/src/+/main:base/process/kill_mac.cc;l=35-69?
|
|
||||||
int kq = HANDLE_EINTR(kqueue());
|
|
||||||
struct kevent change = {0};
|
|
||||||
EV_SET(&change, pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL);
|
|
||||||
ret = HANDLE_EINTR(kevent(kq, &change, 1, NULL, 0, NULL));
|
|
||||||
if (ret == -1) {
|
|
||||||
if (errno == ESRCH) {
|
|
||||||
// At this point, one of the following has occurred:
|
|
||||||
// 1. The process has died but has not yet been reaped.
|
|
||||||
// 2. The process has died and has already been reaped.
|
|
||||||
// 3. The process is in the process of dying. It's no longer
|
|
||||||
// kqueueable, but it may not be waitable yet either. Mark calls
|
|
||||||
// this case the "zombie death race".
|
|
||||||
ret = HANDLE_EINTR(waitpid(pid, &stat_loc, WNOHANG));
|
|
||||||
if (ret == 0) {
|
|
||||||
ret = kill(pid, SIGKILL);
|
|
||||||
if (ret != -1) {
|
|
||||||
HANDLE_EINTR(waitpid(pid, &stat_loc, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
struct kevent event = {0};
|
|
||||||
ret = HANDLE_EINTR(kevent(kq, NULL, 0, &event, 1, NULL));
|
|
||||||
if (ret == 1) {
|
|
||||||
if ((event.fflags & NOTE_EXIT) &&
|
|
||||||
(event.ident == static_cast<uintptr_t>(pid))) {
|
|
||||||
// The process is dead or dying. This won't block for long, if at
|
|
||||||
// all.
|
|
||||||
HANDLE_EINTR(waitpid(pid, &stat_loc, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
while (true) {
|
|
||||||
errno = 0;
|
|
||||||
if ((ret = waitpid(pid, &stat_loc, 0)) != pid) {
|
|
||||||
if (ret == -1 && errno == EINTR) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ret == -1 && errno == ECHILD) {
|
|
||||||
// XXX node v0.8.x seems to have this problem.
|
|
||||||
// waitpid is already handled elsewhere.
|
|
||||||
;
|
|
||||||
} else {
|
|
||||||
assert(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
ExitEvent *exit_event = new ExitEvent;
|
|
||||||
if (WIFEXITED(stat_loc)) {
|
|
||||||
exit_event->exit_code = WEXITSTATUS(stat_loc); // errno?
|
|
||||||
}
|
|
||||||
if (WIFSIGNALED(stat_loc)) {
|
|
||||||
exit_event->signal_code = WTERMSIG(stat_loc);
|
|
||||||
}
|
|
||||||
auto status = tsfn.BlockingCall(exit_event, callback); // In main thread
|
|
||||||
assert(status == napi_ok);
|
|
||||||
|
|
||||||
tsfn.Release();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Methods
|
|
||||||
*/
|
|
||||||
|
|
||||||
Napi::Value PtyFork(const Napi::CallbackInfo& info);
|
|
||||||
Napi::Value PtyOpen(const Napi::CallbackInfo& info);
|
|
||||||
Napi::Value PtyResize(const Napi::CallbackInfo& info);
|
|
||||||
Napi::Value PtyGetProc(const Napi::CallbackInfo& info);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int
|
|
||||||
pty_nonblock(int);
|
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
static char *
|
|
||||||
pty_getproc(int);
|
|
||||||
#else
|
|
||||||
static char *
|
|
||||||
pty_getproc(int, char *);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(__APPLE__) || defined(__OpenBSD__)
|
|
||||||
static void
|
|
||||||
pty_posix_spawn(char** argv, char** env,
|
|
||||||
const struct termios *termp,
|
|
||||||
const struct winsize *winp,
|
|
||||||
int* master,
|
|
||||||
pid_t* pid,
|
|
||||||
int* err);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct DelBuf {
|
|
||||||
int len;
|
|
||||||
DelBuf(int len) : len(len) {}
|
|
||||||
void operator()(char **p) {
|
|
||||||
if (p == nullptr)
|
|
||||||
return;
|
|
||||||
for (int i = 0; i < len; i++)
|
|
||||||
free(p[i]);
|
|
||||||
delete[] p;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Napi::Value PtyFork(const Napi::CallbackInfo& info) {
|
|
||||||
Napi::Env napiEnv(info.Env());
|
|
||||||
Napi::HandleScope scope(napiEnv);
|
|
||||||
|
|
||||||
if (info.Length() != 11 ||
|
|
||||||
!info[0].IsString() ||
|
|
||||||
!info[1].IsArray() ||
|
|
||||||
!info[2].IsArray() ||
|
|
||||||
!info[3].IsString() ||
|
|
||||||
!info[4].IsNumber() ||
|
|
||||||
!info[5].IsNumber() ||
|
|
||||||
!info[6].IsNumber() ||
|
|
||||||
!info[7].IsNumber() ||
|
|
||||||
!info[8].IsBoolean() ||
|
|
||||||
!info[9].IsString() ||
|
|
||||||
!info[10].IsFunction()) {
|
|
||||||
throw Napi::Error::New(napiEnv, "Usage: pty.fork(file, args, env, cwd, cols, rows, uid, gid, utf8, helperPath, onexit)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// file
|
|
||||||
std::string file = info[0].As<Napi::String>();
|
|
||||||
|
|
||||||
// args
|
|
||||||
Napi::Array argv_ = info[1].As<Napi::Array>();
|
|
||||||
|
|
||||||
// env
|
|
||||||
Napi::Array env_ = info[2].As<Napi::Array>();
|
|
||||||
int envc = env_.Length();
|
|
||||||
std::unique_ptr<char *, DelBuf> env_unique_ptr(new char *[envc + 1], DelBuf(envc + 1));
|
|
||||||
char **env = env_unique_ptr.get();
|
|
||||||
env[envc] = NULL;
|
|
||||||
for (int i = 0; i < envc; i++) {
|
|
||||||
std::string pair = env_.Get(i).As<Napi::String>();
|
|
||||||
env[i] = strdup(pair.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// cwd
|
|
||||||
std::string cwd_ = info[3].As<Napi::String>();
|
|
||||||
|
|
||||||
// size
|
|
||||||
struct winsize winp;
|
|
||||||
winp.ws_col = info[4].As<Napi::Number>().Int32Value();
|
|
||||||
winp.ws_row = info[5].As<Napi::Number>().Int32Value();
|
|
||||||
winp.ws_xpixel = 0;
|
|
||||||
winp.ws_ypixel = 0;
|
|
||||||
|
|
||||||
#if !defined(__APPLE__)
|
|
||||||
// uid / gid
|
|
||||||
int uid = info[6].As<Napi::Number>().Int32Value();
|
|
||||||
int gid = info[7].As<Napi::Number>().Int32Value();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// termios
|
|
||||||
struct termios t = termios();
|
|
||||||
struct termios *term = &t;
|
|
||||||
term->c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT;
|
|
||||||
if (info[8].As<Napi::Boolean>().Value()) {
|
|
||||||
#if defined(IUTF8)
|
|
||||||
term->c_iflag |= IUTF8;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
term->c_oflag = OPOST | ONLCR;
|
|
||||||
term->c_cflag = CREAD | CS8 | HUPCL;
|
|
||||||
term->c_lflag = ICANON | ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOKE | ECHOCTL;
|
|
||||||
|
|
||||||
term->c_cc[VEOF] = 4;
|
|
||||||
term->c_cc[VEOL] = -1;
|
|
||||||
term->c_cc[VEOL2] = -1;
|
|
||||||
term->c_cc[VERASE] = 0x7f;
|
|
||||||
term->c_cc[VWERASE] = 23;
|
|
||||||
term->c_cc[VKILL] = 21;
|
|
||||||
term->c_cc[VREPRINT] = 18;
|
|
||||||
term->c_cc[VINTR] = 3;
|
|
||||||
term->c_cc[VQUIT] = 0x1c;
|
|
||||||
term->c_cc[VSUSP] = 26;
|
|
||||||
term->c_cc[VSTART] = 17;
|
|
||||||
term->c_cc[VSTOP] = 19;
|
|
||||||
term->c_cc[VLNEXT] = 22;
|
|
||||||
term->c_cc[VDISCARD] = 15;
|
|
||||||
term->c_cc[VMIN] = 1;
|
|
||||||
term->c_cc[VTIME] = 0;
|
|
||||||
|
|
||||||
#if (__APPLE__)
|
|
||||||
term->c_cc[VDSUSP] = 25;
|
|
||||||
term->c_cc[VSTATUS] = 20;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
cfsetispeed(term, B38400);
|
|
||||||
cfsetospeed(term, B38400);
|
|
||||||
|
|
||||||
// helperPath
|
|
||||||
std::string helper_path = info[9].As<Napi::String>();
|
|
||||||
|
|
||||||
pid_t pid;
|
|
||||||
int master;
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
int argc = argv_.Length();
|
|
||||||
int argl = argc + 4;
|
|
||||||
std::unique_ptr<char *, DelBuf> argv_unique_ptr(new char *[argl], DelBuf(argl));
|
|
||||||
char **argv = argv_unique_ptr.get();
|
|
||||||
argv[0] = strdup(helper_path.c_str());
|
|
||||||
argv[1] = strdup(cwd_.c_str());
|
|
||||||
argv[2] = strdup(file.c_str());
|
|
||||||
argv[argl - 1] = NULL;
|
|
||||||
for (int i = 0; i < argc; i++) {
|
|
||||||
std::string arg = argv_.Get(i).As<Napi::String>();
|
|
||||||
argv[i + 3] = strdup(arg.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
int err = -1;
|
|
||||||
pty_posix_spawn(argv, env, term, &winp, &master, &pid, &err);
|
|
||||||
if (err != 0) {
|
|
||||||
throw Napi::Error::New(napiEnv, "posix_spawnp failed.");
|
|
||||||
}
|
|
||||||
if (pty_nonblock(master) == -1) {
|
|
||||||
throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking.");
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
int argc = argv_.Length();
|
|
||||||
int argl = argc + 2;
|
|
||||||
std::unique_ptr<char *, DelBuf> argv_unique_ptr(new char *[argl], DelBuf(argl));
|
|
||||||
char** argv = argv_unique_ptr.get();
|
|
||||||
argv[0] = strdup(file.c_str());
|
|
||||||
argv[argl - 1] = NULL;
|
|
||||||
for (int i = 0; i < argc; i++) {
|
|
||||||
std::string arg = argv_.Get(i).As<Napi::String>();
|
|
||||||
argv[i + 1] = strdup(arg.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
sigset_t newmask, oldmask;
|
|
||||||
struct sigaction sig_action;
|
|
||||||
// temporarily block all signals
|
|
||||||
// this is needed due to a race condition in openpty
|
|
||||||
// and to avoid running signal handlers in the child
|
|
||||||
// before exec* happened
|
|
||||||
sigfillset(&newmask);
|
|
||||||
pthread_sigmask(SIG_SETMASK, &newmask, &oldmask);
|
|
||||||
|
|
||||||
pid = forkpty(&master, nullptr, static_cast<termios*>(term), static_cast<winsize*>(&winp));
|
|
||||||
|
|
||||||
if (!pid) {
|
|
||||||
// remove all signal handler from child
|
|
||||||
sig_action.sa_handler = SIG_DFL;
|
|
||||||
sig_action.sa_flags = 0;
|
|
||||||
sigemptyset(&sig_action.sa_mask);
|
|
||||||
for (int i = 0 ; i < NSIG ; i++) { // NSIG is a macro for all signals + 1
|
|
||||||
sigaction(i, &sig_action, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-enable signals
|
|
||||||
pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
|
|
||||||
|
|
||||||
switch (pid) {
|
|
||||||
case -1:
|
|
||||||
throw Napi::Error::New(napiEnv, "forkpty(3) failed.");
|
|
||||||
case 0:
|
|
||||||
if (strlen(cwd_.c_str())) {
|
|
||||||
if (chdir(cwd_.c_str()) == -1) {
|
|
||||||
perror("chdir(2) failed.");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uid != -1 && gid != -1) {
|
|
||||||
if (setgid(gid) == -1) {
|
|
||||||
perror("setgid(2) failed.");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
if (setuid(uid) == -1) {
|
|
||||||
perror("setuid(2) failed.");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
char **old = environ;
|
|
||||||
environ = env;
|
|
||||||
execvp(argv[0], argv);
|
|
||||||
environ = old;
|
|
||||||
perror("execvp(3) failed.");
|
|
||||||
_exit(1);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if (pty_nonblock(master) == -1) {
|
|
||||||
throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Napi::Object obj = Napi::Object::New(napiEnv);
|
|
||||||
obj.Set("fd", Napi::Number::New(napiEnv, master));
|
|
||||||
obj.Set("pid", Napi::Number::New(napiEnv, pid));
|
|
||||||
obj.Set("pty", Napi::String::New(napiEnv, ptsname(master)));
|
|
||||||
|
|
||||||
// Set up process exit callback.
|
|
||||||
Napi::Function cb = info[10].As<Napi::Function>();
|
|
||||||
SetupExitCallback(napiEnv, cb, pid);
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
Napi::Value PtyOpen(const Napi::CallbackInfo& info) {
|
|
||||||
Napi::Env env(info.Env());
|
|
||||||
Napi::HandleScope scope(env);
|
|
||||||
|
|
||||||
if (info.Length() != 2 ||
|
|
||||||
!info[0].IsNumber() ||
|
|
||||||
!info[1].IsNumber()) {
|
|
||||||
throw Napi::Error::New(env, "Usage: pty.open(cols, rows)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// size
|
|
||||||
struct winsize winp;
|
|
||||||
winp.ws_col = info[0].As<Napi::Number>().Int32Value();
|
|
||||||
winp.ws_row = info[1].As<Napi::Number>().Int32Value();
|
|
||||||
winp.ws_xpixel = 0;
|
|
||||||
winp.ws_ypixel = 0;
|
|
||||||
|
|
||||||
// pty
|
|
||||||
int master, slave;
|
|
||||||
int ret = openpty(&master, &slave, nullptr, NULL, static_cast<winsize*>(&winp));
|
|
||||||
|
|
||||||
if (ret == -1) {
|
|
||||||
throw Napi::Error::New(env, "openpty(3) failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pty_nonblock(master) == -1) {
|
|
||||||
throw Napi::Error::New(env, "Could not set master fd to nonblocking.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pty_nonblock(slave) == -1) {
|
|
||||||
throw Napi::Error::New(env, "Could not set slave fd to nonblocking.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Napi::Object obj = Napi::Object::New(env);
|
|
||||||
obj.Set("master", Napi::Number::New(env, master));
|
|
||||||
obj.Set("slave", Napi::Number::New(env, slave));
|
|
||||||
obj.Set("pty", Napi::String::New(env, ptsname(master)));
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
Napi::Value PtyResize(const Napi::CallbackInfo& info) {
|
|
||||||
Napi::Env env(info.Env());
|
|
||||||
Napi::HandleScope scope(env);
|
|
||||||
|
|
||||||
if (info.Length() != 3 ||
|
|
||||||
!info[0].IsNumber() ||
|
|
||||||
!info[1].IsNumber() ||
|
|
||||||
!info[2].IsNumber()) {
|
|
||||||
throw Napi::Error::New(env, "Usage: pty.resize(fd, cols, rows)");
|
|
||||||
}
|
|
||||||
|
|
||||||
int fd = info[0].As<Napi::Number>().Int32Value();
|
|
||||||
|
|
||||||
struct winsize winp;
|
|
||||||
winp.ws_col = info[1].As<Napi::Number>().Int32Value();
|
|
||||||
winp.ws_row = info[2].As<Napi::Number>().Int32Value();
|
|
||||||
winp.ws_xpixel = 0;
|
|
||||||
winp.ws_ypixel = 0;
|
|
||||||
|
|
||||||
if (ioctl(fd, TIOCSWINSZ, &winp) == -1) {
|
|
||||||
switch (errno) {
|
|
||||||
case EBADF:
|
|
||||||
throw Napi::Error::New(env, "ioctl(2) failed, EBADF");
|
|
||||||
case EFAULT:
|
|
||||||
throw Napi::Error::New(env, "ioctl(2) failed, EFAULT");
|
|
||||||
case EINVAL:
|
|
||||||
throw Napi::Error::New(env, "ioctl(2) failed, EINVAL");
|
|
||||||
case ENOTTY:
|
|
||||||
throw Napi::Error::New(env, "ioctl(2) failed, ENOTTY");
|
|
||||||
}
|
|
||||||
throw Napi::Error::New(env, "ioctl(2) failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
return env.Undefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Foreground Process Name
|
|
||||||
*/
|
|
||||||
Napi::Value PtyGetProc(const Napi::CallbackInfo& info) {
|
|
||||||
Napi::Env env(info.Env());
|
|
||||||
Napi::HandleScope scope(env);
|
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
if (info.Length() != 1 ||
|
|
||||||
!info[0].IsNumber()) {
|
|
||||||
throw Napi::Error::New(env, "Usage: pty.process(pid)");
|
|
||||||
}
|
|
||||||
|
|
||||||
int fd = info[0].As<Napi::Number>().Int32Value();
|
|
||||||
char *name = pty_getproc(fd);
|
|
||||||
#else
|
|
||||||
if (info.Length() != 2 ||
|
|
||||||
!info[0].IsNumber() ||
|
|
||||||
!info[1].IsString()) {
|
|
||||||
throw Napi::Error::New(env, "Usage: pty.process(fd, tty)");
|
|
||||||
}
|
|
||||||
|
|
||||||
int fd = info[0].As<Napi::Number>().Int32Value();
|
|
||||||
|
|
||||||
std::string tty_ = info[1].As<Napi::String>();
|
|
||||||
char *tty = strdup(tty_.c_str());
|
|
||||||
char *name = pty_getproc(fd, tty);
|
|
||||||
free(tty);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (name == NULL) {
|
|
||||||
return env.Undefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
Napi::String name_ = Napi::String::New(env, name);
|
|
||||||
free(name);
|
|
||||||
return name_;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nonblocking FD
|
|
||||||
*/
|
|
||||||
|
|
||||||
static int
|
|
||||||
pty_nonblock(int fd) {
|
|
||||||
int flags = fcntl(fd, F_GETFL, 0);
|
|
||||||
if (flags == -1) return -1;
|
|
||||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pty_getproc
|
|
||||||
* Taken from tmux.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Taken from: tmux (http://tmux.sourceforge.net/)
|
|
||||||
// Copyright (c) 2009 Nicholas Marriott <nicm@users.sourceforge.net>
|
|
||||||
// Copyright (c) 2009 Joshua Elsasser <josh@elsasser.org>
|
|
||||||
// Copyright (c) 2009 Todd Carson <toc@daybefore.net>
|
|
||||||
//
|
|
||||||
// Permission to use, copy, modify, and distribute this software for any
|
|
||||||
// purpose with or without fee is hereby granted, provided that the above
|
|
||||||
// copyright notice and this permission notice appear in all copies.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
// WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
|
|
||||||
// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
#if defined(__linux__)
|
|
||||||
|
|
||||||
static char *
|
|
||||||
pty_getproc(int fd, char *tty) {
|
|
||||||
FILE *f;
|
|
||||||
char *path, *buf;
|
|
||||||
size_t len;
|
|
||||||
int ch;
|
|
||||||
pid_t pgrp;
|
|
||||||
int r;
|
|
||||||
|
|
||||||
if ((pgrp = tcgetpgrp(fd)) == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
r = asprintf(&path, "/proc/%lld/cmdline", (long long)pgrp);
|
|
||||||
if (r == -1 || path == NULL) return NULL;
|
|
||||||
|
|
||||||
if ((f = fopen(path, "r")) == NULL) {
|
|
||||||
free(path);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
free(path);
|
|
||||||
|
|
||||||
len = 0;
|
|
||||||
buf = NULL;
|
|
||||||
while ((ch = fgetc(f)) != EOF) {
|
|
||||||
if (ch == '\0') break;
|
|
||||||
buf = (char *)realloc(buf, len + 2);
|
|
||||||
if (buf == NULL) return NULL;
|
|
||||||
buf[len++] = ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buf != NULL) {
|
|
||||||
buf[len] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
|
|
||||||
static char *
|
|
||||||
pty_getproc(int fd) {
|
|
||||||
int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, 0 };
|
|
||||||
size_t size;
|
|
||||||
struct kinfo_proc kp;
|
|
||||||
|
|
||||||
if ((mib[3] = tcgetpgrp(fd)) == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
size = sizeof kp;
|
|
||||||
if (sysctl(mib, 4, &kp, &size, NULL, 0) == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size != (sizeof kp) || *kp.kp_proc.p_comm == '\0') {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return strdup(kp.kp_proc.p_comm);
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
|
||||||
|
|
||||||
static char *
|
|
||||||
pty_getproc(int fd, char *tty) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
static void
|
|
||||||
pty_posix_spawn(char** argv, char** env,
|
|
||||||
const struct termios *termp,
|
|
||||||
const struct winsize *winp,
|
|
||||||
int* master,
|
|
||||||
pid_t* pid,
|
|
||||||
int* err) {
|
|
||||||
int low_fds[3];
|
|
||||||
size_t count = 0;
|
|
||||||
|
|
||||||
for (; count < 3; count++) {
|
|
||||||
low_fds[count] = posix_openpt(O_RDWR);
|
|
||||||
if (low_fds[count] >= STDERR_FILENO)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
int flags = POSIX_SPAWN_CLOEXEC_DEFAULT |
|
|
||||||
POSIX_SPAWN_SETSIGDEF |
|
|
||||||
POSIX_SPAWN_SETSIGMASK |
|
|
||||||
POSIX_SPAWN_SETSID;
|
|
||||||
*master = posix_openpt(O_RDWR);
|
|
||||||
if (*master == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int res = grantpt(*master) || unlockpt(*master);
|
|
||||||
if (res == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use TIOCPTYGNAME instead of ptsname() to avoid threading problems.
|
|
||||||
int slave;
|
|
||||||
char slave_pty_name[128];
|
|
||||||
res = ioctl(*master, TIOCPTYGNAME, slave_pty_name);
|
|
||||||
if (res == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
slave = open(slave_pty_name, O_RDWR | O_NOCTTY);
|
|
||||||
if (slave == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (termp) {
|
|
||||||
res = tcsetattr(slave, TCSANOW, termp);
|
|
||||||
if (res == -1) {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (winp) {
|
|
||||||
res = ioctl(slave, TIOCSWINSZ, winp);
|
|
||||||
if (res == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
posix_spawn_file_actions_t acts;
|
|
||||||
posix_spawn_file_actions_init(&acts);
|
|
||||||
posix_spawn_file_actions_adddup2(&acts, slave, STDIN_FILENO);
|
|
||||||
posix_spawn_file_actions_adddup2(&acts, slave, STDOUT_FILENO);
|
|
||||||
posix_spawn_file_actions_adddup2(&acts, slave, STDERR_FILENO);
|
|
||||||
posix_spawn_file_actions_addclose(&acts, slave);
|
|
||||||
posix_spawn_file_actions_addclose(&acts, *master);
|
|
||||||
|
|
||||||
posix_spawnattr_t attrs;
|
|
||||||
posix_spawnattr_init(&attrs);
|
|
||||||
*err = posix_spawnattr_setflags(&attrs, flags);
|
|
||||||
if (*err != 0) {
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
|
|
||||||
sigset_t signal_set;
|
|
||||||
/* Reset all signal the child to their default behavior */
|
|
||||||
sigfillset(&signal_set);
|
|
||||||
*err = posix_spawnattr_setsigdefault(&attrs, &signal_set);
|
|
||||||
if (*err != 0) {
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset the signal mask for all signals */
|
|
||||||
sigemptyset(&signal_set);
|
|
||||||
*err = posix_spawnattr_setsigmask(&attrs, &signal_set);
|
|
||||||
if (*err != 0) {
|
|
||||||
goto done;
|
|
||||||
}
|
|
||||||
|
|
||||||
do
|
|
||||||
*err = posix_spawn(pid, argv[0], &acts, &attrs, argv, env);
|
|
||||||
while (*err == EINTR);
|
|
||||||
done:
|
|
||||||
posix_spawn_file_actions_destroy(&acts);
|
|
||||||
posix_spawnattr_destroy(&attrs);
|
|
||||||
|
|
||||||
for (; count > 0; count--) {
|
|
||||||
close(low_fds[count]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init
|
|
||||||
*/
|
|
||||||
|
|
||||||
Napi::Object init(Napi::Env env, Napi::Object exports) {
|
|
||||||
exports.Set("fork", Napi::Function::New(env, PtyFork));
|
|
||||||
exports.Set("open", Napi::Function::New(env, PtyOpen));
|
|
||||||
exports.Set("resize", Napi::Function::New(env, PtyResize));
|
|
||||||
exports.Set("process", Napi::Function::New(env, PtyGetProc));
|
|
||||||
return exports;
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_API_MODULE(NODE_GYP_MODULE_NAME, init)
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -1,654 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021-2023 Leroy Hopson <godot-xterm@leroy.geek.nz>
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
#include "terminal.h"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <godot_cpp/classes/display_server.hpp>
|
|
||||||
#include <godot_cpp/classes/input.hpp>
|
|
||||||
#include <godot_cpp/classes/input_event_mouse_motion.hpp>
|
|
||||||
#include <godot_cpp/classes/rendering_server.hpp>
|
|
||||||
#include <godot_cpp/classes/resource_loader.hpp>
|
|
||||||
#include <godot_cpp/classes/theme.hpp>
|
|
||||||
#include <godot_cpp/classes/theme_db.hpp>
|
|
||||||
#include <godot_cpp/classes/viewport_texture.hpp>
|
|
||||||
#include <godot_cpp/core/object.hpp>
|
|
||||||
#include <godot_cpp/variant/color.hpp>
|
|
||||||
#include <godot_cpp/variant/dictionary.hpp>
|
|
||||||
#include <string>
|
|
||||||
#include <xkbcommon/xkbcommon-keysyms.h>
|
|
||||||
|
|
||||||
#define UNICODE_MAX 0x10FFFF
|
|
||||||
|
|
||||||
using namespace godot;
|
|
||||||
|
|
||||||
Terminal::Terminal() {
|
|
||||||
// Ensure we write to terminal before the frame is drawn. Otherwise, the
|
|
||||||
// terminal state may be updated but not drawn until it is updated again,
|
|
||||||
// which may not happen for some time.
|
|
||||||
RenderingServer::get_singleton()->connect("frame_pre_draw",
|
|
||||||
Callable(this, "_flush"));
|
|
||||||
|
|
||||||
// Override default focus mode.
|
|
||||||
set_focus_mode(FOCUS_ALL);
|
|
||||||
|
|
||||||
// Name our nodes for easier debugging.
|
|
||||||
back_buffer->set_name("BackBuffer");
|
|
||||||
sub_viewport->set_name("SubViewport");
|
|
||||||
front_buffer->set_name("FrontBuffer");
|
|
||||||
|
|
||||||
// Ensure buffers always have correct size.
|
|
||||||
back_buffer->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
|
|
||||||
front_buffer->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
|
|
||||||
|
|
||||||
// Setup back buffer.
|
|
||||||
back_buffer->connect("draw", Callable(this, "_on_back_buffer_draw"));
|
|
||||||
|
|
||||||
// Setup sub viewport.
|
|
||||||
sub_viewport->set_handle_input_locally(false);
|
|
||||||
sub_viewport->set_transparent_background(true);
|
|
||||||
sub_viewport->set_snap_controls_to_pixels(false);
|
|
||||||
sub_viewport->set_update_mode(SubViewport::UPDATE_WHEN_PARENT_VISIBLE);
|
|
||||||
sub_viewport->set_clear_mode(SubViewport::CLEAR_MODE_NEVER);
|
|
||||||
sub_viewport->add_child(back_buffer);
|
|
||||||
add_child(sub_viewport);
|
|
||||||
|
|
||||||
// Setup bell timer.
|
|
||||||
bell_timer->set_name("BellTimer");
|
|
||||||
bell_timer->set_one_shot(true);
|
|
||||||
add_child(bell_timer);
|
|
||||||
|
|
||||||
// Setup blink timer.
|
|
||||||
blink_timer->set_name("BlinkTimer");
|
|
||||||
blink_timer->set_one_shot(true);
|
|
||||||
blink_timer->connect("timeout", Callable(this, "_toggle_blink"));
|
|
||||||
add_child(blink_timer);
|
|
||||||
|
|
||||||
// Setup selection timer.
|
|
||||||
selection_timer->set_name("SelectionTimer");
|
|
||||||
selection_timer->set_wait_time(0.05);
|
|
||||||
selection_timer->connect("timeout", Callable(this, "_on_selection_held"));
|
|
||||||
add_child(selection_timer);
|
|
||||||
|
|
||||||
// Setup front buffer.
|
|
||||||
front_buffer->set_texture(sub_viewport->get_texture());
|
|
||||||
add_child(front_buffer);
|
|
||||||
|
|
||||||
framebuffer_age = 0;
|
|
||||||
update_mode = UpdateMode::AUTO;
|
|
||||||
|
|
||||||
if (tsm_screen_new(&screen, NULL, NULL)) {
|
|
||||||
ERR_PRINT("Error creating new tsm screen.");
|
|
||||||
}
|
|
||||||
tsm_screen_set_max_sb(screen, 1000);
|
|
||||||
|
|
||||||
if (tsm_vte_new(&vte, screen, &Terminal::_write_cb, this, NULL, NULL)) {
|
|
||||||
ERR_PRINT("Error creating new tsm vte.");
|
|
||||||
}
|
|
||||||
|
|
||||||
tsm_vte_set_bell_cb(vte, &Terminal::_bell_cb, this);
|
|
||||||
|
|
||||||
_update_theme_item_cache();
|
|
||||||
}
|
|
||||||
|
|
||||||
Terminal::~Terminal() {
|
|
||||||
back_buffer->queue_free();
|
|
||||||
sub_viewport->queue_free();
|
|
||||||
front_buffer->queue_free();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::set_copy_on_selection(bool value) { copy_on_selection = value; }
|
|
||||||
|
|
||||||
bool Terminal::get_copy_on_selection() { return copy_on_selection; }
|
|
||||||
|
|
||||||
void Terminal::set_update_mode(Terminal::UpdateMode value) {
|
|
||||||
update_mode = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
Terminal::UpdateMode Terminal::get_update_mode() { return update_mode; }
|
|
||||||
|
|
||||||
void Terminal::set_bell_cooldown(double value) { bell_cooldown = value; }
|
|
||||||
|
|
||||||
double Terminal::get_bell_cooldown() { return bell_cooldown; }
|
|
||||||
|
|
||||||
void Terminal::set_bell_muted(bool value) { bell_muted = value; }
|
|
||||||
|
|
||||||
bool Terminal::get_bell_muted() { return bell_muted; }
|
|
||||||
|
|
||||||
void Terminal::set_blink_enabled(bool value) {
|
|
||||||
blink_enabled = value;
|
|
||||||
|
|
||||||
if (!blink_enabled)
|
|
||||||
blink_timer->stop();
|
|
||||||
|
|
||||||
_refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Terminal::get_blink_enabled() { return blink_enabled; }
|
|
||||||
|
|
||||||
void Terminal::set_blink_time_off(double value) {
|
|
||||||
blink_time_off = value;
|
|
||||||
|
|
||||||
if (!blink_on && !blink_timer->is_stopped()) {
|
|
||||||
double time_left = blink_timer->get_time_left();
|
|
||||||
blink_timer->start(std::min(blink_time_off, time_left));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double Terminal::get_blink_time_off() { return blink_time_off; }
|
|
||||||
|
|
||||||
void Terminal::set_blink_time_on(double value) {
|
|
||||||
blink_time_on = value;
|
|
||||||
|
|
||||||
if (blink_on && !blink_timer->is_stopped()) {
|
|
||||||
double time_left = blink_timer->get_time_left();
|
|
||||||
blink_timer->start(std::min(blink_time_on, time_left));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double Terminal::get_blink_time_on() { return blink_time_on; }
|
|
||||||
|
|
||||||
void Terminal::clear() {
|
|
||||||
Vector2 initial_size = get_size();
|
|
||||||
set_size(Vector2(initial_size.x, cell_size.y));
|
|
||||||
tsm_screen_clear_sb(screen);
|
|
||||||
set_size(initial_size);
|
|
||||||
back_buffer->queue_redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
String Terminal::copy_all() {
|
|
||||||
char *out = nullptr;
|
|
||||||
int len = tsm_screen_copy_all(screen, &out);
|
|
||||||
String result = String(out);
|
|
||||||
std::free(out);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
String Terminal::copy_selection() {
|
|
||||||
char *out = nullptr;
|
|
||||||
int len = tsm_screen_selection_copy(screen, &out);
|
|
||||||
String result = String(out);
|
|
||||||
std::free(out);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
int Terminal::get_cols() { return cols; }
|
|
||||||
int Terminal::get_rows() { return rows; }
|
|
||||||
|
|
||||||
void Terminal::write(Variant data) {
|
|
||||||
switch (data.get_type()) {
|
|
||||||
case Variant::PACKED_BYTE_ARRAY:
|
|
||||||
break;
|
|
||||||
case Variant::STRING:
|
|
||||||
data = ((String)data).to_utf8_buffer();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ERR_PRINT("Expected data to be a String or PackedByteArray.");
|
|
||||||
}
|
|
||||||
|
|
||||||
write_buffer.push_back(data);
|
|
||||||
|
|
||||||
queue_redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_gui_input(Ref<InputEvent> event) {
|
|
||||||
_handle_key_input(event);
|
|
||||||
_handle_selection(event);
|
|
||||||
_handle_mouse_wheel(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_notification(int what) {
|
|
||||||
switch (what) {
|
|
||||||
case NOTIFICATION_RESIZED:
|
|
||||||
_recalculate_size();
|
|
||||||
sub_viewport->set_size(get_size());
|
|
||||||
_refresh();
|
|
||||||
break;
|
|
||||||
case NOTIFICATION_THEME_CHANGED:
|
|
||||||
_update_theme_item_cache();
|
|
||||||
_refresh();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_flush() {
|
|
||||||
if (write_buffer.is_empty())
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (int i = 0; i < write_buffer.size(); i++) {
|
|
||||||
PackedByteArray data = static_cast<PackedByteArray>(write_buffer[i]);
|
|
||||||
tsm_vte_input(vte, (char *)data.ptr(), data.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
write_buffer.clear();
|
|
||||||
|
|
||||||
back_buffer->queue_redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_on_back_buffer_draw() {
|
|
||||||
if (update_mode == UpdateMode::DISABLED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((update_mode > UpdateMode::AUTO) || framebuffer_age == 0) {
|
|
||||||
Color background_color = palette[TSM_COLOR_BACKGROUND];
|
|
||||||
back_buffer->draw_rect(back_buffer->get_rect(), background_color);
|
|
||||||
}
|
|
||||||
|
|
||||||
int prev_framebuffer_age = framebuffer_age;
|
|
||||||
framebuffer_age = tsm_screen_draw(screen, &Terminal::_text_draw_cb, this);
|
|
||||||
|
|
||||||
if (update_mode == UpdateMode::ALL_NEXT_FRAME && prev_framebuffer_age != 0)
|
|
||||||
update_mode = UpdateMode::AUTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_on_selection_held() {
|
|
||||||
if (!(Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) ||
|
|
||||||
selection_mode == SelectionMode::NONE) {
|
|
||||||
if (copy_on_selection)
|
|
||||||
DisplayServer::get_singleton()->clipboard_set_primary(copy_selection());
|
|
||||||
selection_timer->stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector2 target = get_local_mouse_position() / cell_size;
|
|
||||||
tsm_screen_selection_target(screen, target.x, target.y);
|
|
||||||
back_buffer->queue_redraw();
|
|
||||||
selection_timer->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_toggle_blink() {
|
|
||||||
if (blink_enabled) {
|
|
||||||
blink_on = !blink_on;
|
|
||||||
_refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_bind_methods() {
|
|
||||||
// Properties.
|
|
||||||
ClassDB::bind_method(D_METHOD("set_copy_on_selection", "value"),
|
|
||||||
&Terminal::set_copy_on_selection);
|
|
||||||
ClassDB::bind_method(D_METHOD("get_copy_on_selection"),
|
|
||||||
&Terminal::get_copy_on_selection);
|
|
||||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "copy_on_selection"),
|
|
||||||
"set_copy_on_selection", "get_copy_on_selection");
|
|
||||||
ClassDB::bind_method(D_METHOD("set_update_mode", "value"),
|
|
||||||
&Terminal::set_update_mode);
|
|
||||||
ClassDB::bind_method(D_METHOD("get_update_mode"), &Terminal::get_update_mode);
|
|
||||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "update_mode", PROPERTY_HINT_ENUM,
|
|
||||||
"Disabled,Auto,All,All Next Frame"),
|
|
||||||
"set_update_mode", "get_update_mode");
|
|
||||||
|
|
||||||
ADD_GROUP("Bell", "bell_");
|
|
||||||
ClassDB::bind_method(D_METHOD("set_bell_cooldown", "value"),
|
|
||||||
&Terminal::set_bell_cooldown);
|
|
||||||
ClassDB::bind_method(D_METHOD("get_bell_cooldown"),
|
|
||||||
&Terminal::get_bell_cooldown);
|
|
||||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "bell_cooldown"),
|
|
||||||
"set_bell_cooldown", "get_bell_cooldown");
|
|
||||||
ClassDB::bind_method(D_METHOD("set_bell_muted", "value"),
|
|
||||||
&Terminal::set_bell_muted);
|
|
||||||
ClassDB::bind_method(D_METHOD("get_bell_muted"), &Terminal::get_bell_muted);
|
|
||||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "bell_muted"), "set_bell_muted",
|
|
||||||
"get_bell_muted");
|
|
||||||
|
|
||||||
ADD_GROUP("Blink", "blink_");
|
|
||||||
ClassDB::bind_method(D_METHOD("set_blink_enabled", "value"),
|
|
||||||
&Terminal::set_blink_enabled);
|
|
||||||
ClassDB::bind_method(D_METHOD("get_blink_enabled"),
|
|
||||||
&Terminal::get_blink_enabled);
|
|
||||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "blink_enabled"),
|
|
||||||
"set_blink_enabled", "get_blink_enabled");
|
|
||||||
ClassDB::bind_method(D_METHOD("get_blink_time_off"),
|
|
||||||
&Terminal::get_blink_time_off);
|
|
||||||
ClassDB::bind_method(D_METHOD("set_blink_time_off", "value"),
|
|
||||||
&Terminal::set_blink_time_off);
|
|
||||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "blink_time_off"),
|
|
||||||
"set_blink_time_off", "get_blink_time_off");
|
|
||||||
ClassDB::bind_method(D_METHOD("set_blink_time_on", "value"),
|
|
||||||
&Terminal::set_blink_time_on);
|
|
||||||
ClassDB::bind_method(D_METHOD("get_blink_time_on"),
|
|
||||||
&Terminal::get_blink_time_on);
|
|
||||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "blink_time_on"),
|
|
||||||
"set_blink_time_on", "get_blink_time_on");
|
|
||||||
|
|
||||||
// Methods.
|
|
||||||
ClassDB::bind_method(D_METHOD("clear"), &Terminal::clear);
|
|
||||||
ClassDB::bind_method(D_METHOD("copy_all"), &Terminal::copy_all);
|
|
||||||
ClassDB::bind_method(D_METHOD("copy_selection"), &Terminal::copy_selection);
|
|
||||||
ClassDB::bind_method(D_METHOD("get_cols"), &Terminal::get_cols);
|
|
||||||
ClassDB::bind_method(D_METHOD("get_rows"), &Terminal::get_rows);
|
|
||||||
ClassDB::bind_method(D_METHOD("write", "data"), &Terminal::write);
|
|
||||||
|
|
||||||
// Signals.
|
|
||||||
ADD_SIGNAL(MethodInfo("bell"));
|
|
||||||
ADD_SIGNAL(MethodInfo("data_sent",
|
|
||||||
PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data")));
|
|
||||||
ADD_SIGNAL(MethodInfo("key_pressed",
|
|
||||||
PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data"),
|
|
||||||
PropertyInfo(Variant::OBJECT, "event")));
|
|
||||||
ADD_SIGNAL(
|
|
||||||
MethodInfo("size_changed", PropertyInfo(Variant::VECTOR2, "new_size")));
|
|
||||||
|
|
||||||
// Enumerations.
|
|
||||||
BIND_ENUM_CONSTANT(UPDATE_MODE_DISABLED);
|
|
||||||
BIND_ENUM_CONSTANT(UPDATE_MODE_AUTO);
|
|
||||||
BIND_ENUM_CONSTANT(UPDATE_MODE_ALL);
|
|
||||||
BIND_ENUM_CONSTANT(UPDATE_MODE_ALL_NEXT_FRAME);
|
|
||||||
|
|
||||||
// Private methods (must be exposed as they are connected to signals).
|
|
||||||
ClassDB::bind_method(D_METHOD("_flush"), &Terminal::_flush);
|
|
||||||
ClassDB::bind_method(D_METHOD("_on_back_buffer_draw"),
|
|
||||||
&Terminal::_on_back_buffer_draw);
|
|
||||||
ClassDB::bind_method(D_METHOD("_on_selection_held"),
|
|
||||||
&Terminal::_on_selection_held);
|
|
||||||
ClassDB::bind_method(D_METHOD("_toggle_blink"), &Terminal::_toggle_blink);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_bell_cb(tsm_vte *vte, void *data) {
|
|
||||||
Terminal *term = static_cast<Terminal *>(data);
|
|
||||||
|
|
||||||
if (!term->bell_muted && term->bell_cooldown == 0 ||
|
|
||||||
term->bell_timer->get_time_left() == 0) {
|
|
||||||
term->emit_signal("bell");
|
|
||||||
if (term->bell_cooldown > 0)
|
|
||||||
term->bell_timer->start(term->bell_cooldown);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int Terminal::_text_draw_cb(tsm_screen *con, uint64_t id, const uint32_t *ch,
|
|
||||||
size_t len, unsigned int width, unsigned int col,
|
|
||||||
unsigned int row, const tsm_screen_attr *attr,
|
|
||||||
tsm_age_t age, void *data) {
|
|
||||||
Terminal *term = static_cast<Terminal *>(data);
|
|
||||||
if (term->update_mode == Terminal::UpdateMode::AUTO && age != 0 &&
|
|
||||||
age <= term->framebuffer_age) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width < 1) { // No foreground or background to draw.
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ColorPair color_pair = term->_get_cell_colors(attr);
|
|
||||||
term->_draw_background(row, col, color_pair.first, width);
|
|
||||||
|
|
||||||
if (len < 1) // No foreground to draw.
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
size_t ulen = 0;
|
|
||||||
char buf[5] = {0};
|
|
||||||
|
|
||||||
char *utf8 = tsm_ucs4_to_utf8_alloc(ch, len, &ulen);
|
|
||||||
memcpy(buf, utf8, ulen);
|
|
||||||
term->_draw_foreground(row, col, buf, attr, color_pair.second);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_write_cb(tsm_vte *vte, const char *u8, size_t len, void *data) {
|
|
||||||
Terminal *term = static_cast<Terminal *>(data);
|
|
||||||
|
|
||||||
PackedByteArray bytes;
|
|
||||||
bytes.resize(len);
|
|
||||||
{ memcpy(bytes.ptrw(), u8, len); }
|
|
||||||
|
|
||||||
if (len > 0) {
|
|
||||||
if (term->last_input_event_key.is_valid()) {
|
|
||||||
// The callback was fired from a key press event so emit the "key_pressed"
|
|
||||||
// signal.
|
|
||||||
term->emit_signal("key_pressed", bytes.get_string_from_utf8(),
|
|
||||||
term->last_input_event_key);
|
|
||||||
term->last_input_event_key.unref();
|
|
||||||
}
|
|
||||||
|
|
||||||
term->emit_signal("data_sent", bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_draw_background(int row, int col, Color bgcolor,
|
|
||||||
int width = 1) {
|
|
||||||
/* Draw the background */
|
|
||||||
Vector2 background_pos = Vector2(col * cell_size.x, row * cell_size.y);
|
|
||||||
Rect2 background_rect = Rect2(background_pos, cell_size * Vector2(width, 1));
|
|
||||||
back_buffer->draw_rect(background_rect, bgcolor);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_draw_foreground(int row, int col, char *ch,
|
|
||||||
const tsm_screen_attr *attr, Color fgcolor) {
|
|
||||||
Ref<Font> font;
|
|
||||||
|
|
||||||
if (attr->bold && attr->italic) {
|
|
||||||
font = theme_cache.fonts["bold_italics_font"];
|
|
||||||
} else if (attr->bold) {
|
|
||||||
font = theme_cache.fonts["bold_font"];
|
|
||||||
} else if (attr->italic) {
|
|
||||||
font = theme_cache.fonts["italics_font"];
|
|
||||||
} else {
|
|
||||||
font = theme_cache.fonts["normal_font"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attr->blink && blink_enabled) {
|
|
||||||
if (blink_timer->is_stopped())
|
|
||||||
blink_timer->start(blink_on ? blink_time_on : blink_time_off);
|
|
||||||
|
|
||||||
if (!blink_on)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int font_height = font->get_height(theme_cache.font_size);
|
|
||||||
Vector2 foreground_pos =
|
|
||||||
Vector2(col * cell_size.x, row * cell_size.y + font_height / 1.25);
|
|
||||||
back_buffer->draw_string(font, foreground_pos, ch, HORIZONTAL_ALIGNMENT_LEFT,
|
|
||||||
-1, theme_cache.font_size, fgcolor);
|
|
||||||
|
|
||||||
if (attr->underline)
|
|
||||||
back_buffer->draw_string(font, foreground_pos, "_",
|
|
||||||
HORIZONTAL_ALIGNMENT_LEFT, -1,
|
|
||||||
theme_cache.font_size, fgcolor);
|
|
||||||
}
|
|
||||||
|
|
||||||
Terminal::ColorPair Terminal::_get_cell_colors(const tsm_screen_attr *attr) {
|
|
||||||
Color fgcol, bgcol;
|
|
||||||
int8_t fccode = attr->fccode;
|
|
||||||
int8_t bccode = attr->bccode;
|
|
||||||
|
|
||||||
// Get foreground color.
|
|
||||||
if (fccode && palette.count(fccode)) {
|
|
||||||
fgcol = palette[fccode];
|
|
||||||
} else {
|
|
||||||
fgcol = Color(attr->fr / 255.0f, attr->fg / 255.0f, attr->fb / 255.0f);
|
|
||||||
|
|
||||||
if (fccode != -1)
|
|
||||||
palette.insert({fccode, fgcol});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get background color.
|
|
||||||
if (bccode && palette.count(bccode)) {
|
|
||||||
bgcol = palette[bccode];
|
|
||||||
} else {
|
|
||||||
bgcol = Color(attr->br / 255.0f, attr->bg / 255.0f, attr->bb / 255.0f);
|
|
||||||
|
|
||||||
if (bccode != -1)
|
|
||||||
palette.insert({bccode, bgcol});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attr->inverse)
|
|
||||||
std::swap(bgcol, fgcol);
|
|
||||||
|
|
||||||
return std::make_pair(bgcol, fgcol);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_handle_key_input(Ref<InputEventKey> event) {
|
|
||||||
if (!event.is_valid() || !event->is_pressed())
|
|
||||||
return;
|
|
||||||
|
|
||||||
const Key keycode = event->get_keycode();
|
|
||||||
char32_t unicode = event->get_unicode();
|
|
||||||
uint32_t ascii = unicode <= 127 ? unicode : 0;
|
|
||||||
|
|
||||||
unsigned int mods = 0;
|
|
||||||
if (event->is_alt_pressed())
|
|
||||||
mods |= TSM_ALT_MASK;
|
|
||||||
if (event->is_ctrl_pressed())
|
|
||||||
mods |= TSM_CONTROL_MASK;
|
|
||||||
if (event->is_shift_pressed())
|
|
||||||
mods |= TSM_SHIFT_MASK;
|
|
||||||
|
|
||||||
std::pair<Key, char32_t> key = {keycode, unicode};
|
|
||||||
uint32_t keysym =
|
|
||||||
(KEY_MAP.count(key) > 0) ? KEY_MAP.at(key) : XKB_KEY_NoSymbol;
|
|
||||||
|
|
||||||
last_input_event_key = event;
|
|
||||||
tsm_vte_handle_keyboard(vte, keysym, ascii, mods,
|
|
||||||
unicode ? unicode : TSM_VTE_INVALID);
|
|
||||||
|
|
||||||
// Return to the bottom of the scrollback buffer if we scrolled up. Ignore
|
|
||||||
// modifier keys pressed in isolation or if Ctrl+Shift modifier keys are
|
|
||||||
// pressed.
|
|
||||||
std::set<Key> mod_keys = {KEY_ALT, KEY_SHIFT, KEY_CTRL, KEY_META};
|
|
||||||
if (mod_keys.find(keycode) == mod_keys.end() &&
|
|
||||||
!(event->is_ctrl_pressed() && event->is_shift_pressed())) {
|
|
||||||
tsm_screen_sb_reset(screen);
|
|
||||||
back_buffer->queue_redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent focus changing to other inputs when pressing Tab or Arrow keys.
|
|
||||||
std::set<Key> tab_arrow_keys = {KEY_LEFT, KEY_UP, KEY_RIGHT, KEY_DOWN,
|
|
||||||
KEY_TAB};
|
|
||||||
if (tab_arrow_keys.find(keycode) != tab_arrow_keys.end())
|
|
||||||
accept_event();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_handle_mouse_wheel(Ref<InputEventMouseButton> event) {
|
|
||||||
if (!event.is_valid() || !event->is_pressed())
|
|
||||||
return;
|
|
||||||
|
|
||||||
void (*scroll_func)(tsm_screen *, unsigned int) = nullptr;
|
|
||||||
|
|
||||||
switch (event->get_button_index()) {
|
|
||||||
case MOUSE_BUTTON_WHEEL_UP:
|
|
||||||
scroll_func = &tsm_screen_sb_up;
|
|
||||||
break;
|
|
||||||
case MOUSE_BUTTON_WHEEL_DOWN:
|
|
||||||
scroll_func = &tsm_screen_sb_down;
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (scroll_func != nullptr) {
|
|
||||||
// Scroll 5 times as fast as normal if alt is pressed (like TextEdit).
|
|
||||||
// Otherwise, just scroll 3 lines.
|
|
||||||
int speed = event->is_alt_pressed() ? 15 : 3;
|
|
||||||
double factor = event->get_factor();
|
|
||||||
(*scroll_func)(screen, speed * factor);
|
|
||||||
back_buffer->queue_redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_handle_selection(Ref<InputEventMouse> event) {
|
|
||||||
if (!event.is_valid())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Ref<InputEventMouseButton> mb = event;
|
|
||||||
if (mb.is_valid()) {
|
|
||||||
if (!mb->is_pressed() || !mb->get_button_index() == MOUSE_BUTTON_LEFT)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (selecting) {
|
|
||||||
selecting = false;
|
|
||||||
selection_mode = SelectionMode::NONE;
|
|
||||||
tsm_screen_selection_reset(screen);
|
|
||||||
back_buffer->queue_redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
selecting = false;
|
|
||||||
selection_mode = SelectionMode::POINTER;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ref<InputEventMouseMotion> mm = event;
|
|
||||||
if (mm.is_valid()) {
|
|
||||||
if ((mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT) &&
|
|
||||||
selection_mode != SelectionMode::NONE && !selecting) {
|
|
||||||
selecting = true;
|
|
||||||
Vector2 start = event->get_position() / cell_size;
|
|
||||||
tsm_screen_selection_start(screen, start.x, start.y);
|
|
||||||
back_buffer->queue_redraw();
|
|
||||||
selection_timer->start();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_recalculate_size() {
|
|
||||||
Vector2 size = get_size();
|
|
||||||
|
|
||||||
cell_size = theme_cache.fonts["normal_font"]->get_string_size(
|
|
||||||
"W", HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size);
|
|
||||||
|
|
||||||
rows = std::max(1, (int)floor(size.y / cell_size.y));
|
|
||||||
cols = std::max(1, (int)floor(size.x / cell_size.x));
|
|
||||||
|
|
||||||
tsm_screen_resize(screen, cols, rows);
|
|
||||||
sub_viewport->set_size(size);
|
|
||||||
|
|
||||||
emit_signal("size_changed", Vector2(cols, rows));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_refresh() {
|
|
||||||
back_buffer->queue_redraw();
|
|
||||||
front_buffer->queue_redraw();
|
|
||||||
|
|
||||||
if (update_mode == UpdateMode::AUTO)
|
|
||||||
update_mode = UpdateMode::ALL_NEXT_FRAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Terminal::_update_theme_item_cache() {
|
|
||||||
// Fonts.
|
|
||||||
for (std::map<const char *, const char *>::const_iterator iter =
|
|
||||||
Terminal::FONTS.begin();
|
|
||||||
iter != Terminal::FONTS.end(); ++iter) {
|
|
||||||
String name = iter->first;
|
|
||||||
|
|
||||||
Ref<Font> font = has_theme_font_override(name) ? get_theme_font(name)
|
|
||||||
: has_theme_font(name, "Terminal")
|
|
||||||
? get_theme_font(name, "Terminal")
|
|
||||||
: ThemeDB::get_singleton()->get_fallback_font();
|
|
||||||
|
|
||||||
theme_cache.fonts[name] = font;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Font size.
|
|
||||||
theme_cache.font_size =
|
|
||||||
has_theme_font_size_override("font_size")
|
|
||||||
? get_theme_font_size("font_size")
|
|
||||||
: has_theme_font_size("font_size", "Terminal")
|
|
||||||
? get_theme_font_size("font_size", "Terminal")
|
|
||||||
: ThemeDB::get_singleton()->get_fallback_font_size();
|
|
||||||
|
|
||||||
// Colors.
|
|
||||||
uint8_t custom_palette[TSM_COLOR_NUM][3];
|
|
||||||
|
|
||||||
for (ColorMap::const_iterator iter = Terminal::COLORS.begin();
|
|
||||||
iter != Terminal::COLORS.end(); ++iter) {
|
|
||||||
String name = iter->first;
|
|
||||||
|
|
||||||
Color color = has_theme_color_override(name) ? get_theme_color(name)
|
|
||||||
: has_theme_color(name, "Terminal")
|
|
||||||
? get_theme_color(name, "Terminal")
|
|
||||||
: color = Color::html(iter->second.default_color);
|
|
||||||
|
|
||||||
theme_cache.colors[name] = color;
|
|
||||||
palette[iter->second.tsm_color] = color;
|
|
||||||
custom_palette[iter->second.tsm_color][0] = color.get_r8();
|
|
||||||
custom_palette[iter->second.tsm_color][1] = color.get_g8();
|
|
||||||
custom_palette[iter->second.tsm_color][2] = color.get_b8();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tsm_vte_set_custom_palette(vte, custom_palette))
|
|
||||||
ERR_PRINT("Error setting custom palette.");
|
|
||||||
if (tsm_vte_set_palette(vte, "custom"))
|
|
||||||
ERR_PRINT("Error setting palette to custom palette.");
|
|
||||||
|
|
||||||
_recalculate_size();
|
|
||||||
}
|
|
|
@ -1,164 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021-2023 Leroy Hopson <godot-xterm@leroy.geek.nz>
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
#ifndef GODOT_XTERM_TERMINAL_H
|
|
||||||
#define GODOT_XTERM_TERMINAL_H
|
|
||||||
|
|
||||||
#include <godot_cpp/classes/control.hpp>
|
|
||||||
#include <godot_cpp/classes/font.hpp>
|
|
||||||
#include <godot_cpp/classes/input_event.hpp>
|
|
||||||
#include <godot_cpp/classes/input_event_key.hpp>
|
|
||||||
#include <godot_cpp/classes/input_event_mouse.hpp>
|
|
||||||
#include <godot_cpp/classes/input_event_mouse_button.hpp>
|
|
||||||
#include <godot_cpp/classes/sub_viewport.hpp>
|
|
||||||
#include <godot_cpp/classes/texture_rect.hpp>
|
|
||||||
#include <godot_cpp/classes/timer.hpp>
|
|
||||||
#include <godot_cpp/variant/packed_byte_array.hpp>
|
|
||||||
#include <libtsm.h>
|
|
||||||
#include <map>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using namespace godot;
|
|
||||||
|
|
||||||
class Terminal : public Control {
|
|
||||||
GDCLASS(Terminal, Control)
|
|
||||||
|
|
||||||
public:
|
|
||||||
Terminal();
|
|
||||||
~Terminal();
|
|
||||||
|
|
||||||
enum UpdateMode {
|
|
||||||
DISABLED,
|
|
||||||
AUTO,
|
|
||||||
ALL,
|
|
||||||
ALL_NEXT_FRAME,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const UpdateMode UPDATE_MODE_DISABLED = UpdateMode::DISABLED;
|
|
||||||
static const UpdateMode UPDATE_MODE_AUTO = UpdateMode::AUTO;
|
|
||||||
static const UpdateMode UPDATE_MODE_ALL = UpdateMode::ALL;
|
|
||||||
static const UpdateMode UPDATE_MODE_ALL_NEXT_FRAME =
|
|
||||||
UpdateMode::ALL_NEXT_FRAME;
|
|
||||||
|
|
||||||
bool copy_on_selection = false;
|
|
||||||
void set_copy_on_selection(bool value);
|
|
||||||
bool get_copy_on_selection();
|
|
||||||
|
|
||||||
UpdateMode update_mode = UPDATE_MODE_AUTO;
|
|
||||||
void set_update_mode(UpdateMode value);
|
|
||||||
UpdateMode get_update_mode();
|
|
||||||
|
|
||||||
double bell_cooldown = 0.1f;
|
|
||||||
void set_bell_cooldown(double value);
|
|
||||||
double get_bell_cooldown();
|
|
||||||
|
|
||||||
bool bell_muted = false;
|
|
||||||
void set_bell_muted(bool value);
|
|
||||||
bool get_bell_muted();
|
|
||||||
|
|
||||||
bool blink_enabled = true;
|
|
||||||
void set_blink_enabled(bool value);
|
|
||||||
bool get_blink_enabled();
|
|
||||||
|
|
||||||
double blink_time_off = 0.3;
|
|
||||||
void set_blink_time_off(double value);
|
|
||||||
double get_blink_time_off();
|
|
||||||
|
|
||||||
double blink_time_on = 0.6;
|
|
||||||
void set_blink_time_on(double value);
|
|
||||||
double get_blink_time_on();
|
|
||||||
|
|
||||||
void clear();
|
|
||||||
String copy_all();
|
|
||||||
String copy_selection();
|
|
||||||
int get_cols();
|
|
||||||
int get_rows();
|
|
||||||
void write(Variant data);
|
|
||||||
|
|
||||||
void _gui_input(Ref<InputEvent> event);
|
|
||||||
void _notification(int what);
|
|
||||||
|
|
||||||
void _flush();
|
|
||||||
void _on_back_buffer_draw();
|
|
||||||
void _on_selection_held();
|
|
||||||
void _toggle_blink();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
static void _bind_methods();
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct ColorDef {
|
|
||||||
const char *default_color;
|
|
||||||
tsm_vte_color tsm_color;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ThemeCache {
|
|
||||||
int font_size = 0;
|
|
||||||
std::map<String, Ref<Font>> fonts = std::map<String, Ref<Font>>{};
|
|
||||||
std::map<String, Color> colors = std::map<String, Color>{};
|
|
||||||
} theme_cache;
|
|
||||||
|
|
||||||
typedef std::map<const char *, ColorDef> ColorMap;
|
|
||||||
typedef std::pair<Color, Color> ColorPair;
|
|
||||||
typedef std::map<const char *, const char *> FontMap;
|
|
||||||
typedef std::map<std::pair<Key, char32_t>, uint32_t> KeyMap;
|
|
||||||
|
|
||||||
static const KeyMap KEY_MAP;
|
|
||||||
static const ColorMap COLORS;
|
|
||||||
static const FontMap FONTS;
|
|
||||||
|
|
||||||
enum SelectionMode { NONE, POINTER };
|
|
||||||
|
|
||||||
Control *back_buffer = new Control();
|
|
||||||
SubViewport *sub_viewport = new SubViewport();
|
|
||||||
TextureRect *front_buffer = new TextureRect();
|
|
||||||
|
|
||||||
Timer *bell_timer = new Timer();
|
|
||||||
Timer *blink_timer = new Timer();
|
|
||||||
Timer *selection_timer = new Timer();
|
|
||||||
|
|
||||||
tsm_screen *screen;
|
|
||||||
tsm_vte *vte;
|
|
||||||
|
|
||||||
Array write_buffer = Array();
|
|
||||||
|
|
||||||
int cols = 80;
|
|
||||||
int rows = 24;
|
|
||||||
|
|
||||||
// Whether blinking characters are visible. Not whether blinking is enabled
|
|
||||||
// which is determined by `blink_enabled`.
|
|
||||||
bool blink_on = true;
|
|
||||||
|
|
||||||
Vector2 cell_size = Vector2(0, 0);
|
|
||||||
|
|
||||||
std::map<int, Color> palette = {};
|
|
||||||
|
|
||||||
tsm_age_t framebuffer_age;
|
|
||||||
|
|
||||||
Ref<InputEventKey> last_input_event_key;
|
|
||||||
|
|
||||||
bool selecting = false;
|
|
||||||
SelectionMode selection_mode = SelectionMode::NONE;
|
|
||||||
|
|
||||||
static void _bell_cb(tsm_vte *vte, void *data);
|
|
||||||
static int _text_draw_cb(tsm_screen *con, uint64_t id, const uint32_t *ch,
|
|
||||||
size_t len, unsigned int width, unsigned int col,
|
|
||||||
unsigned int row, const tsm_screen_attr *attr,
|
|
||||||
tsm_age_t age, void *data);
|
|
||||||
static void _write_cb(tsm_vte *vte, const char *u8, size_t len, void *data);
|
|
||||||
|
|
||||||
void _draw_background(int row, int col, Color bgcol, int width);
|
|
||||||
void _draw_foreground(int row, int col, char *ch, const tsm_screen_attr *attr,
|
|
||||||
Color fgcol);
|
|
||||||
ColorPair _get_cell_colors(const tsm_screen_attr *attr);
|
|
||||||
void _handle_key_input(Ref<InputEventKey> event);
|
|
||||||
void _handle_mouse_wheel(Ref<InputEventMouseButton> event);
|
|
||||||
void _handle_selection(Ref<InputEventMouse> event);
|
|
||||||
void _recalculate_size();
|
|
||||||
void _refresh();
|
|
||||||
void _update_theme_item_cache();
|
|
||||||
};
|
|
||||||
|
|
||||||
VARIANT_ENUM_CAST(Terminal::UpdateMode);
|
|
||||||
|
|
||||||
#endif // GODOT_XTERM_TERMINAL_H
|
|
Loading…
Reference in a new issue