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:
Leroy Hopson 2024-03-30 02:03:31 +13:00
parent c4f0afc4f2
commit d7c83b3fe9
No known key found for this signature in database
GPG key ID: D2747312A6DB51AA
20 changed files with 0 additions and 4588 deletions

View file

@ -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
}
}

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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.

View file

@ -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() {}

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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_

View file

@ -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);

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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();
}

View file

@ -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