mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
fix: handle authenticate-pam as optional dependency in npm package
- Modified postinstall script to extract authenticate-pam prebuilds when npm skips optional deps - Updated authenticate-pam-loader to load from optional-modules directory - Prepared beta.12 release with all fixes consolidated - Moved Docker test script to scripts folder - Updated documentation in npm.md
This commit is contained in:
parent
693565d9ea
commit
84b7467e83
49 changed files with 5609 additions and 14 deletions
293
web/package/README.md
Normal file
293
web/package/README.md
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
# VibeTunnel CLI
|
||||||
|
|
||||||
|
**Turn any browser into your terminal.** VibeTunnel proxies your terminals right into the browser, so you can vibe-code anywhere.
|
||||||
|
|
||||||
|
Full-featured terminal sharing server with web interface for macOS and Linux. Windows not yet supported.
|
||||||
|
|
||||||
|
## Why VibeTunnel?
|
||||||
|
|
||||||
|
Ever wanted to check on your AI agents while you're away? Need to monitor that long-running build from your phone? Want to share a terminal session with a colleague without complex SSH setups? VibeTunnel makes it happen with zero friction.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From npm (Recommended)
|
||||||
|
```bash
|
||||||
|
npm install -g vibetunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/amantus-ai/vibetunnel.git
|
||||||
|
cd vibetunnel/web
|
||||||
|
pnpm install
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation Differences
|
||||||
|
|
||||||
|
**npm package**:
|
||||||
|
- Pre-built binaries for common platforms (macOS x64/arm64, Linux x64/arm64)
|
||||||
|
- Automatic fallback to source compilation if pre-built binaries unavailable
|
||||||
|
- Global installation makes `vibetunnel` and `vt` commands available system-wide
|
||||||
|
- Includes production dependencies only
|
||||||
|
|
||||||
|
**Source installation**:
|
||||||
|
- Full development environment with hot reload (`pnpm run dev`)
|
||||||
|
- Access to all development scripts and tools
|
||||||
|
- Ability to modify and rebuild the application
|
||||||
|
- Includes test suites and development dependencies
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js >= 20.0.0
|
||||||
|
- macOS or Linux (Windows not yet supported)
|
||||||
|
- Build tools for native modules (Xcode on macOS, build-essential on Linux)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Start the server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start with default settings (port 4020)
|
||||||
|
vibetunnel
|
||||||
|
|
||||||
|
# Start with custom port
|
||||||
|
vibetunnel --port 8080
|
||||||
|
|
||||||
|
# Start without authentication
|
||||||
|
vibetunnel --no-auth
|
||||||
|
|
||||||
|
# Bind to specific interface
|
||||||
|
vibetunnel --bind 127.0.0.1 --port 4020
|
||||||
|
|
||||||
|
# Enable SSH key authentication
|
||||||
|
vibetunnel --enable-ssh-keys
|
||||||
|
|
||||||
|
# SSH keys only (no password auth)
|
||||||
|
vibetunnel --disallow-user-password
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open http://localhost:4020 in your browser to access the web interface.
|
||||||
|
|
||||||
|
### Command-line Options
|
||||||
|
|
||||||
|
```
|
||||||
|
vibetunnel [options]
|
||||||
|
|
||||||
|
Basic Options:
|
||||||
|
--help, -h Show help message
|
||||||
|
--version, -v Show version information
|
||||||
|
--port <number> Server port (default: 4020 or PORT env var)
|
||||||
|
--bind <address> Bind address (default: 0.0.0.0, all interfaces)
|
||||||
|
|
||||||
|
Authentication Options:
|
||||||
|
--no-auth Disable authentication (auto-login as current user)
|
||||||
|
--enable-ssh-keys Enable SSH key authentication UI and functionality
|
||||||
|
--disallow-user-password Disable password auth, SSH keys only (auto-enables --enable-ssh-keys)
|
||||||
|
--allow-local-bypass Allow localhost connections to bypass authentication
|
||||||
|
--local-auth-token <token> Token for localhost authentication bypass
|
||||||
|
|
||||||
|
Push Notification Options:
|
||||||
|
--push-enabled Enable push notifications (default: enabled)
|
||||||
|
--push-disabled Disable push notifications
|
||||||
|
--vapid-email <email> Contact email for VAPID configuration
|
||||||
|
--generate-vapid-keys Generate new VAPID keys if none exist
|
||||||
|
|
||||||
|
Network Discovery Options:
|
||||||
|
--no-mdns Disable mDNS/Bonjour advertisement (enabled by default)
|
||||||
|
|
||||||
|
HQ Mode Options:
|
||||||
|
--hq Run as HQ (headquarters) server
|
||||||
|
--no-hq-auth Disable HQ authentication
|
||||||
|
|
||||||
|
Remote Server Options:
|
||||||
|
--hq-url <url> HQ server URL to register with
|
||||||
|
--hq-username <user> Username for HQ authentication
|
||||||
|
--hq-password <pass> Password for HQ authentication
|
||||||
|
--name <name> Unique name for remote server
|
||||||
|
--allow-insecure-hq Allow HTTP URLs for HQ (not recommended)
|
||||||
|
|
||||||
|
Repository Options:
|
||||||
|
--repository-base-path <path> Base path for repository discovery
|
||||||
|
|
||||||
|
Debugging:
|
||||||
|
--debug Enable debug logging
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use the vt command wrapper
|
||||||
|
|
||||||
|
The `vt` command allows you to run commands with TTY forwarding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Monitor AI agents with automatic activity tracking
|
||||||
|
vt claude
|
||||||
|
vt claude --dangerously-skip-permissions
|
||||||
|
|
||||||
|
# Run commands with output visible in VibeTunnel
|
||||||
|
vt npm test
|
||||||
|
vt python script.py
|
||||||
|
vt top
|
||||||
|
|
||||||
|
# Launch interactive shell
|
||||||
|
vt --shell
|
||||||
|
vt -i
|
||||||
|
|
||||||
|
# Update session title (inside a session)
|
||||||
|
vt title "My Project"
|
||||||
|
|
||||||
|
# Execute command directly without shell wrapper
|
||||||
|
vt --no-shell-wrap ls -la
|
||||||
|
vt -S ls -la
|
||||||
|
|
||||||
|
# Control terminal title behavior
|
||||||
|
vt --title-mode none # No title management
|
||||||
|
vt --title-mode filter # Block all title changes
|
||||||
|
vt --title-mode static # Show directory and command
|
||||||
|
vt --title-mode dynamic # Show directory, command, and activity
|
||||||
|
|
||||||
|
# Verbosity control
|
||||||
|
vt -q npm test # Quiet mode (errors only)
|
||||||
|
vt -v npm run dev # Verbose mode
|
||||||
|
vt -vv npm test # Extra verbose
|
||||||
|
vt -vvv npm build # Debug mode
|
||||||
|
```
|
||||||
|
|
||||||
|
### Forward commands to a session
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic usage
|
||||||
|
vibetunnel fwd <session-id> <command> [args...]
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
vibetunnel fwd --session-id abc123 ls -la
|
||||||
|
vibetunnel fwd --session-id abc123 npm test
|
||||||
|
vibetunnel fwd --session-id abc123 python script.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
VibeTunnel respects the following environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PORT=8080 # Default port if --port not specified
|
||||||
|
VIBETUNNEL_USERNAME=myuser # Username (for env-based auth, not CLI)
|
||||||
|
VIBETUNNEL_PASSWORD=mypass # Password (for env-based auth, not CLI)
|
||||||
|
VIBETUNNEL_CONTROL_DIR=/path # Control directory for session data
|
||||||
|
VIBETUNNEL_SESSION_ID=abc123 # Current session ID (set automatically inside sessions)
|
||||||
|
VIBETUNNEL_LOG_LEVEL=debug # Log level: error, warn, info, verbose, debug
|
||||||
|
PUSH_CONTACT_EMAIL=admin@example.com # Contact email for VAPID configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Web-based terminal interface** - Access terminals from any browser
|
||||||
|
- **Multiple concurrent sessions** - Run multiple terminals simultaneously
|
||||||
|
- **Real-time synchronization** - See output in real-time
|
||||||
|
- **TTY forwarding** - Full terminal emulation support
|
||||||
|
- **Session management** - Create, list, and manage sessions
|
||||||
|
- **Cross-platform** - Works on macOS and Linux
|
||||||
|
- **No dependencies** - Just Node.js required
|
||||||
|
|
||||||
|
## Package Contents
|
||||||
|
|
||||||
|
This npm package includes:
|
||||||
|
- Full VibeTunnel server with web UI
|
||||||
|
- Command-line tools (vibetunnel, vt)
|
||||||
|
- Native PTY support for terminal emulation
|
||||||
|
- Web interface with xterm.js
|
||||||
|
- Session management and forwarding
|
||||||
|
|
||||||
|
## Platform Support
|
||||||
|
|
||||||
|
- macOS (Intel and Apple Silicon)
|
||||||
|
- Linux (x64 and ARM64)
|
||||||
|
- Windows: Not yet supported ([#252](https://github.com/amantus-ai/vibetunnel/issues/252))
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Installation Issues
|
||||||
|
|
||||||
|
If you encounter issues during installation:
|
||||||
|
|
||||||
|
1. **Missing Build Tools**: Install build essentials
|
||||||
|
```bash
|
||||||
|
# Ubuntu/Debian
|
||||||
|
sudo apt-get install build-essential python3-dev
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
xcode-select --install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Permission Issues**: Use sudo for global installation
|
||||||
|
```bash
|
||||||
|
sudo npm install -g vibetunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Node Version**: Ensure Node.js 20+ is installed
|
||||||
|
```bash
|
||||||
|
node --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runtime Issues
|
||||||
|
|
||||||
|
- **Server Won't Start**: Check if port is already in use
|
||||||
|
- **Authentication Failed**: Verify system authentication setup
|
||||||
|
- **Terminal Not Responsive**: Check browser console for WebSocket errors
|
||||||
|
|
||||||
|
### SSH Key Authentication Issues
|
||||||
|
|
||||||
|
If you encounter errors when generating or importing SSH keys (e.g., "Cannot read properties of undefined"), this is due to browser security restrictions on the Web Crypto API.
|
||||||
|
|
||||||
|
#### The Issue
|
||||||
|
Modern browsers (Chrome 60+, Firefox 75+) block the Web Crypto API when accessing web applications over HTTP from non-localhost addresses. This affects:
|
||||||
|
- Local network IPs (192.168.x.x, 10.x.x.x, 172.16-31.x.x)
|
||||||
|
- Any non-localhost hostname over HTTP
|
||||||
|
|
||||||
|
#### Solutions
|
||||||
|
|
||||||
|
1. **Use localhost (Recommended)**
|
||||||
|
```bash
|
||||||
|
# Access VibeTunnel via localhost
|
||||||
|
http://localhost:4020
|
||||||
|
|
||||||
|
# If running on a remote server, use SSH tunneling:
|
||||||
|
ssh -L 4020:localhost:4020 user@your-server
|
||||||
|
# Then access http://localhost:4020 in your browser
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Enable HTTPS**
|
||||||
|
Set up a reverse proxy with HTTPS using nginx or Caddy (recommended for production).
|
||||||
|
|
||||||
|
3. **Chrome Flag Workaround** (Development only)
|
||||||
|
- Navigate to `chrome://flags/#unsafely-treat-insecure-origin-as-secure`
|
||||||
|
- Add your server URL (e.g., `http://192.168.1.100:4020`)
|
||||||
|
- Enable the flag and restart Chrome
|
||||||
|
- ⚠️ This reduces security - use only for development
|
||||||
|
|
||||||
|
#### Why This Happens
|
||||||
|
The Web Crypto API is restricted to secure contexts (HTTPS or localhost) to prevent man-in-the-middle attacks on cryptographic operations. This is a browser security feature, not a VibeTunnel limitation.
|
||||||
|
|
||||||
|
### Development Setup
|
||||||
|
|
||||||
|
For source installations:
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Run development server with hot reload
|
||||||
|
pnpm run dev
|
||||||
|
|
||||||
|
# Run code quality checks
|
||||||
|
pnpm run check
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
See the main repository for complete documentation: https://github.com/amantus-ai/vibetunnel
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
27
web/package/bin/vibetunnel
Executable file
27
web/package/bin/vibetunnel
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Start the CLI - it handles all command routing including 'fwd'
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const cliPath = path.join(__dirname, '..', 'lib', 'vibetunnel-cli');
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
const child = spawn('node', [cliPath, ...args], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: process.env
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
if (signal) {
|
||||||
|
// Process was killed by signal, exit with 128 + signal number convention
|
||||||
|
// Common signals: SIGTERM=15, SIGINT=2, SIGKILL=9
|
||||||
|
const signalExitCode = signal === 'SIGTERM' ? 143 :
|
||||||
|
signal === 'SIGINT' ? 130 :
|
||||||
|
signal === 'SIGKILL' ? 137 : 128;
|
||||||
|
process.exit(signalExitCode);
|
||||||
|
} else {
|
||||||
|
// Normal exit, use the exit code (or 0 if null)
|
||||||
|
process.exit(code ?? 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
343
web/package/bin/vt
Executable file
343
web/package/bin/vt
Executable file
|
|
@ -0,0 +1,343 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Unified VibeTunnel CLI wrapper - compatible with both Mac app and npm installations
|
||||||
|
|
||||||
|
# Only check for Mac app on macOS
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
# macOS symlink resolution function using BSD readlink
|
||||||
|
resolve_symlink_macos() {
|
||||||
|
local target="$1"
|
||||||
|
local current="$target"
|
||||||
|
while [ -L "$current" ]; do
|
||||||
|
current="$(readlink "$current")"
|
||||||
|
# Handle relative symlinks
|
||||||
|
if [[ "$current" != /* ]]; then
|
||||||
|
current="$(dirname "$target")/$current"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "$current"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the real path of this script to avoid infinite recursion
|
||||||
|
SCRIPT_REAL_PATH="$(resolve_symlink_macos "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
|
# Comprehensive Mac app search - try standard locations first, then development locations
|
||||||
|
APP_PATH=""
|
||||||
|
|
||||||
|
# First try standard locations with valid binary check
|
||||||
|
for TRY_PATH in "/Applications/VibeTunnel.app" "$HOME/Applications/VibeTunnel.app"; do
|
||||||
|
if [ -d "$TRY_PATH" ] && [ -f "$TRY_PATH/Contents/Resources/vibetunnel" ]; then
|
||||||
|
VT_SCRIPT="$TRY_PATH/Contents/Resources/vt"
|
||||||
|
if [ -f "$VT_SCRIPT" ] && [ -x "$VT_SCRIPT" ]; then
|
||||||
|
# Avoid infinite recursion by checking if this is the same script
|
||||||
|
VT_REAL_PATH="$(resolve_symlink_macos "$VT_SCRIPT")"
|
||||||
|
if [ "$SCRIPT_REAL_PATH" != "$VT_REAL_PATH" ]; then
|
||||||
|
exec "$VT_SCRIPT" "$@"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
APP_PATH="$TRY_PATH"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# If not found in standard locations, search for development builds
|
||||||
|
if [ -z "$APP_PATH" ]; then
|
||||||
|
# First try DerivedData (for development)
|
||||||
|
for CANDIDATE in $(find ~/Library/Developer/Xcode/DerivedData -name "VibeTunnel.app" -type d 2>/dev/null | grep -v "\.dSYM" | grep -v "Index\.noindex"); do
|
||||||
|
if [ -f "$CANDIDATE/Contents/Resources/vibetunnel" ]; then
|
||||||
|
VT_SCRIPT="$CANDIDATE/Contents/Resources/vt"
|
||||||
|
if [ -f "$VT_SCRIPT" ] && [ -x "$VT_SCRIPT" ]; then
|
||||||
|
VT_REAL_PATH="$(resolve_symlink_macos "$VT_SCRIPT")"
|
||||||
|
if [ "$SCRIPT_REAL_PATH" != "$VT_REAL_PATH" ]; then
|
||||||
|
exec "$VT_SCRIPT" "$@"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
APP_PATH="$CANDIDATE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# If still not found, use mdfind as last resort
|
||||||
|
if [ -z "$APP_PATH" ]; then
|
||||||
|
for CANDIDATE in $(mdfind -name "VibeTunnel.app" 2>/dev/null | grep -v "\.dSYM"); do
|
||||||
|
if [ -f "$CANDIDATE/Contents/Resources/vibetunnel" ]; then
|
||||||
|
VT_SCRIPT="$CANDIDATE/Contents/Resources/vt"
|
||||||
|
if [ -f "$VT_SCRIPT" ] && [ -x "$VT_SCRIPT" ]; then
|
||||||
|
VT_REAL_PATH="$(resolve_symlink_macos "$VT_SCRIPT")"
|
||||||
|
if [ "$SCRIPT_REAL_PATH" != "$VT_REAL_PATH" ]; then
|
||||||
|
exec "$VT_SCRIPT" "$@"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
APP_PATH="$CANDIDATE"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If we found a Mac app but couldn't use its vt script, use its binary directly
|
||||||
|
if [ -n "$APP_PATH" ]; then
|
||||||
|
VIBETUNNEL_BIN="$APP_PATH/Contents/Resources/vibetunnel"
|
||||||
|
if [ -f "$VIBETUNNEL_BIN" ]; then
|
||||||
|
# Found Mac app bundle - will use this binary
|
||||||
|
# Silent operation - no message printed
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If we get here without a Mac app, use the npm-installed vibetunnel
|
||||||
|
if [ -z "$VIBETUNNEL_BIN" ]; then
|
||||||
|
# First, try to find vibetunnel in the same directory as this script
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
if [ -f "$SCRIPT_DIR/vibetunnel" ]; then
|
||||||
|
VIBETUNNEL_BIN="$SCRIPT_DIR/vibetunnel"
|
||||||
|
else
|
||||||
|
# Try to find vibetunnel in PATH
|
||||||
|
if command -v vibetunnel >/dev/null 2>&1; then
|
||||||
|
VIBETUNNEL_BIN="$(command -v vibetunnel)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$VIBETUNNEL_BIN" ] || [ ! -f "$VIBETUNNEL_BIN" ]; then
|
||||||
|
echo "Error: vibetunnel binary not found. Please ensure vibetunnel is installed." >&2
|
||||||
|
echo "Install with: npm install -g vibetunnel" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we're already inside a VibeTunnel session
|
||||||
|
if [ -n "$VIBETUNNEL_SESSION_ID" ]; then
|
||||||
|
# Special case: handle 'vt title' command inside a session
|
||||||
|
if [[ "$1" == "title" ]]; then
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
echo "Error: 'vt title' requires a title argument" >&2
|
||||||
|
echo "Usage: vt title <new title>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift # Remove 'title' from arguments
|
||||||
|
TITLE="$*" # Get all remaining arguments as the title
|
||||||
|
|
||||||
|
# Use the vibetunnel binary's new --update-title flag
|
||||||
|
exec "$VIBETUNNEL_BIN" fwd --update-title "$TITLE" --session-id "$VIBETUNNEL_SESSION_ID"
|
||||||
|
# If exec fails, exit with error
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Error: Already inside a VibeTunnel session (ID: $VIBETUNNEL_SESSION_ID). Recursive VibeTunnel sessions are not supported." >&2
|
||||||
|
echo "If you need to run commands, use them directly without the 'vt' prefix." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to show help
|
||||||
|
show_help() {
|
||||||
|
cat << 'EOF'
|
||||||
|
vt - VibeTunnel TTY Forward Wrapper
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
vt [command] [args...]
|
||||||
|
vt --shell [args...]
|
||||||
|
vt -i [args...]
|
||||||
|
vt --no-shell-wrap [command] [args...]
|
||||||
|
vt -S [command] [args...]
|
||||||
|
vt title <new title> # Inside a VibeTunnel session only
|
||||||
|
vt --help
|
||||||
|
|
||||||
|
QUICK VERBOSITY:
|
||||||
|
-q (quiet), -v (verbose), -vv (extra), -vvv (debug)
|
||||||
|
|
||||||
|
DESCRIPTION:
|
||||||
|
This wrapper script allows VibeTunnel to see the output of commands by
|
||||||
|
forwarding TTY data through the vibetunnel utility. When you run commands
|
||||||
|
through 'vt', VibeTunnel can monitor and display the command's output
|
||||||
|
in real-time.
|
||||||
|
|
||||||
|
By default, commands are executed through your shell to resolve aliases,
|
||||||
|
functions, and builtins. Use --no-shell-wrap to execute commands directly.
|
||||||
|
|
||||||
|
Inside a VibeTunnel session, use 'vt title' to update the session name.
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
vt top # Watch top with VibeTunnel monitoring
|
||||||
|
vt python script.py # Run Python script with output forwarding
|
||||||
|
vt npm test # Run tests with VibeTunnel visibility
|
||||||
|
vt --shell # Launch current shell (equivalent to vt $SHELL)
|
||||||
|
vt -i # Launch current shell (short form)
|
||||||
|
vt -S ls -la # List files without shell alias resolution
|
||||||
|
vt title "My Project" # Update session title (inside session only)
|
||||||
|
vt -q npm test # Run with minimal output (errors only)
|
||||||
|
vt -vv npm run dev # Run with verbose output
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--shell, -i Launch current shell (equivalent to vt $SHELL)
|
||||||
|
--no-shell-wrap, -S Execute command directly without shell wrapper
|
||||||
|
--title-mode <mode> Terminal title mode (none, filter, static, dynamic)
|
||||||
|
Default: none (dynamic for claude)
|
||||||
|
--quiet, -q Quiet mode - only show errors
|
||||||
|
--verbose, -v Verbose mode - show more information
|
||||||
|
-vv Extra verbose - show all except debug
|
||||||
|
-vvv Debug mode - show all messages
|
||||||
|
--help, -h Show this help message and exit
|
||||||
|
|
||||||
|
TITLE MODES:
|
||||||
|
none No title management - apps control their own titles
|
||||||
|
filter Block all title changes from applications
|
||||||
|
static Show working directory and command in title
|
||||||
|
dynamic Show directory, command, and live activity status (default for web UI)
|
||||||
|
|
||||||
|
VERBOSITY:
|
||||||
|
By default, only errors are shown. Use verbosity flags to control output:
|
||||||
|
-q/--quiet Suppress all output except critical errors
|
||||||
|
-v/--verbose Show errors, warnings, and informational messages
|
||||||
|
-vv Show everything except debug messages
|
||||||
|
-vvv Show all messages including debug
|
||||||
|
|
||||||
|
You can also set VIBETUNNEL_LOG_LEVEL environment variable:
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=error # Default
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=warn # Show errors and warnings
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=info # Show errors, warnings, and info
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=verbose # All except debug
|
||||||
|
export VIBETUNNEL_LOG_LEVEL=debug # Everything
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
This script automatically detects and uses the best available VibeTunnel installation:
|
||||||
|
- Mac app bundle (preferred on macOS)
|
||||||
|
- npm package installation (fallback)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Show path and version info
|
||||||
|
echo
|
||||||
|
echo "VIBETUNNEL BINARY:"
|
||||||
|
echo " Path: $VIBETUNNEL_BIN"
|
||||||
|
if [ -f "$VIBETUNNEL_BIN" ]; then
|
||||||
|
# Try to get version from binary output first (works for both Mac app and npm)
|
||||||
|
VERSION_INFO=$("$VIBETUNNEL_BIN" --version 2>&1 | grep "^VibeTunnel Server" | head -n 1)
|
||||||
|
BUILD_INFO=$("$VIBETUNNEL_BIN" --version 2>&1 | grep "^Built:" | head -n 1)
|
||||||
|
PLATFORM_INFO=$("$VIBETUNNEL_BIN" --version 2>&1 | grep "^Platform:" | head -n 1)
|
||||||
|
|
||||||
|
if [ -n "$VERSION_INFO" ]; then
|
||||||
|
echo " Version: ${VERSION_INFO#VibeTunnel Server }"
|
||||||
|
else
|
||||||
|
# Fallback to package.json for npm installations
|
||||||
|
PACKAGE_JSON="$(dirname "$(dirname "$VIBETUNNEL_BIN")")/package.json"
|
||||||
|
if [ -f "$PACKAGE_JSON" ]; then
|
||||||
|
VERSION=$(grep '"version"' "$PACKAGE_JSON" | head -1 | sed 's/.*"version".*:.*"\(.*\)".*/\1/')
|
||||||
|
echo " Version: $VERSION"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$BUILD_INFO" ]; then
|
||||||
|
echo " ${BUILD_INFO}"
|
||||||
|
fi
|
||||||
|
if [ -n "$PLATFORM_INFO" ]; then
|
||||||
|
echo " ${PLATFORM_INFO}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine installation type
|
||||||
|
if [[ "$VIBETUNNEL_BIN" == */Applications/VibeTunnel.app/* ]]; then
|
||||||
|
echo " Status: Mac app bundle"
|
||||||
|
elif [[ "$VIBETUNNEL_BIN" == */DerivedData/* ]]; then
|
||||||
|
echo " Status: Development build"
|
||||||
|
elif [[ "$VIBETUNNEL_BIN" == *npm* ]] || [[ "$VIBETUNNEL_BIN" == */bin/vibetunnel ]]; then
|
||||||
|
echo " Status: Installed via npm"
|
||||||
|
else
|
||||||
|
echo " Status: Unknown installation"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " Status: Not found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to resolve command through user's shell
|
||||||
|
resolve_command() {
|
||||||
|
local user_shell="${SHELL:-/bin/bash}"
|
||||||
|
local cmd="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
local shell_name=$(basename "$user_shell")
|
||||||
|
|
||||||
|
# Always try through shell first to handle aliases, functions, and builtins
|
||||||
|
# The shell will fall back to PATH lookup if no alias/function exists
|
||||||
|
case "$shell_name" in
|
||||||
|
zsh)
|
||||||
|
# For zsh, we need interactive mode to get aliases
|
||||||
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -i -c "$(printf '%q ' "$cmd" "$@")"
|
||||||
|
;;
|
||||||
|
bash)
|
||||||
|
# For bash, expand aliases in non-interactive mode
|
||||||
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -c "shopt -s expand_aliases; source ~/.bashrc 2>/dev/null || source ~/.bash_profile 2>/dev/null || true; $(printf '%q ' "$cmd" "$@")"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Generic shell handling
|
||||||
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$user_shell" -c "$(printf '%q ' "$cmd" "$@")"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle --help or -h option, or no arguments (show help)
|
||||||
|
if [[ $# -eq 0 || "$1" == "--help" || "$1" == "-h" ]]; then
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle 'vt title' command when not inside a session
|
||||||
|
if [[ "$1" == "title" ]]; then
|
||||||
|
echo "Error: 'vt title' can only be used inside a VibeTunnel session." >&2
|
||||||
|
echo "Start a session first with 'vt' or 'vt <command>'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle verbosity flags
|
||||||
|
VERBOSITY_ARGS=""
|
||||||
|
if [[ "$1" == "--quiet" || "$1" == "-q" ]]; then
|
||||||
|
VERBOSITY_ARGS="--verbosity silent"
|
||||||
|
shift
|
||||||
|
elif [[ "$1" == "--verbose" || "$1" == "-v" ]]; then
|
||||||
|
VERBOSITY_ARGS="--verbosity info"
|
||||||
|
shift
|
||||||
|
elif [[ "$1" == "-vv" ]]; then
|
||||||
|
VERBOSITY_ARGS="--verbosity verbose"
|
||||||
|
shift
|
||||||
|
elif [[ "$1" == "-vvv" ]]; then
|
||||||
|
VERBOSITY_ARGS="--verbosity debug"
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle --shell or -i option (launch current shell)
|
||||||
|
if [[ "$1" == "--shell" || "$1" == "-i" ]]; then
|
||||||
|
shift
|
||||||
|
# Execute current shell through vibetunnel
|
||||||
|
exec "$0" ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} "${SHELL:-/bin/bash}" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle --no-shell-wrap or -S option
|
||||||
|
NO_SHELL_WRAP=false
|
||||||
|
if [[ "$1" == "--no-shell-wrap" || "$1" == "-S" ]]; then
|
||||||
|
NO_SHELL_WRAP=true
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle --title-mode option
|
||||||
|
TITLE_MODE_ARGS=""
|
||||||
|
if [[ "$1" == "--title-mode" && $# -gt 1 ]]; then
|
||||||
|
TITLE_MODE_ARGS="--title-mode $2"
|
||||||
|
shift 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we have arguments and if the first argument is not an option
|
||||||
|
if [ $# -gt 0 ] && [[ "$1" != -* ]]; then
|
||||||
|
if [[ "$NO_SHELL_WRAP" == "true" ]]; then
|
||||||
|
# Execute directly without shell wrapper
|
||||||
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@"
|
||||||
|
else
|
||||||
|
# Check if the first argument is a real binary
|
||||||
|
if which "$1" >/dev/null 2>&1; then
|
||||||
|
# It's a real binary, execute directly
|
||||||
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@"
|
||||||
|
else
|
||||||
|
# Not a real binary, try alias resolution
|
||||||
|
resolve_command "$@"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Run with fwd command (original behavior for options)
|
||||||
|
exec "$VIBETUNNEL_BIN" fwd ${VERBOSITY_ARGS:+$VERBOSITY_ARGS} ${TITLE_MODE_ARGS:+"$TITLE_MODE_ARGS"} "$@"
|
||||||
|
fi
|
||||||
2
web/package/lib/cli.js
Executable file
2
web/package/lib/cli.js
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
require('./vibetunnel-cli');
|
||||||
125
web/package/lib/vibetunnel-cli
Executable file
125
web/package/lib/vibetunnel-cli
Executable file
File diff suppressed because one or more lines are too long
47
web/package/node-pty/README.md
Normal file
47
web/package/node-pty/README.md
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Vendored PTY
|
||||||
|
|
||||||
|
This is a vendored fork of [node-pty](https://github.com/microsoft/node-pty) v1.1.0-beta34 with the threading and shared pipe architecture removed.
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
The original node-pty uses a shared pipe/socket architecture through `ConoutSocketWorker` that causes issues when used heavily:
|
||||||
|
- All PTY instances write to the same shared pipe
|
||||||
|
- This can overwhelm other Electron processes (like VS Code) that are also listening on the pipe
|
||||||
|
- Heavy usage from VibeTunnel causes crashes in other applications
|
||||||
|
|
||||||
|
## What's Changed?
|
||||||
|
|
||||||
|
1. **Removed ConoutSocketWorker** - No more worker threads for socket management
|
||||||
|
2. **Removed shared pipe architecture** - Each PTY instance uses direct file descriptors
|
||||||
|
3. **Simplified Windows implementation** - Direct socket connections without intermediary workers
|
||||||
|
4. **Kept core functionality** - The essential PTY spawn/resize/kill operations remain unchanged
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The native modules will be compiled during installation.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The API remains compatible with node-pty:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const pty = require('node-pty');
|
||||||
|
const ptyProcess = pty.spawn('bash', [], {
|
||||||
|
name: 'xterm-color',
|
||||||
|
cols: 80,
|
||||||
|
rows: 30,
|
||||||
|
cwd: process.env.HOME,
|
||||||
|
env: process.env
|
||||||
|
});
|
||||||
|
|
||||||
|
ptyProcess.on('data', function(data) {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
ptyProcess.write('ls\r');
|
||||||
|
```
|
||||||
63
web/package/node-pty/binding.gyp
Normal file
63
web/package/node-pty/binding.gyp
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
{
|
||||||
|
'targets': [{
|
||||||
|
'target_name': 'pty',
|
||||||
|
'include_dirs': [
|
||||||
|
'src/',
|
||||||
|
'<!@(node -p "require(\'node-addon-api\').include")'
|
||||||
|
],
|
||||||
|
'defines': [ 'NAPI_CPP_EXCEPTIONS' ],
|
||||||
|
'cflags!': [ '-fno-exceptions' ],
|
||||||
|
'cflags_cc!': [ '-fno-exceptions' ],
|
||||||
|
'xcode_settings': {
|
||||||
|
'GCC_ENABLE_CPP_EXCEPTIONS': 'YES',
|
||||||
|
'CLANG_CXX_LIBRARY': 'libc++',
|
||||||
|
'MACOSX_DEPLOYMENT_TARGET': '14.0',
|
||||||
|
},
|
||||||
|
'msvs_settings': {
|
||||||
|
'VCCLCompilerTool': { 'ExceptionHandling': 1 },
|
||||||
|
},
|
||||||
|
'conditions': [
|
||||||
|
['OS=="win"', {
|
||||||
|
'sources': [
|
||||||
|
'src/win/conpty.cc',
|
||||||
|
'src/win/path_util.cc'
|
||||||
|
],
|
||||||
|
'libraries': [
|
||||||
|
'-lkernel32.lib',
|
||||||
|
'-luser32.lib',
|
||||||
|
'-lshell32.lib',
|
||||||
|
'-ladvapi32.lib'
|
||||||
|
],
|
||||||
|
'defines': [
|
||||||
|
'_WIN32_WINNT=0x0600',
|
||||||
|
'NTDDI_VERSION=0x06000000'
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
['OS!="win"', {
|
||||||
|
'sources': [
|
||||||
|
'src/unix/pty.cc'
|
||||||
|
],
|
||||||
|
'libraries': [
|
||||||
|
'-lutil'
|
||||||
|
],
|
||||||
|
'conditions': [
|
||||||
|
['OS=="mac"', {
|
||||||
|
'xcode_settings': {
|
||||||
|
'MACOSX_DEPLOYMENT_TARGET': '14.0'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
'target_name': 'spawn-helper',
|
||||||
|
'type': 'executable',
|
||||||
|
'conditions': [
|
||||||
|
['OS!="win"', {
|
||||||
|
'sources': [
|
||||||
|
'src/unix/spawn-helper.cc'
|
||||||
|
],
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
13
web/package/node-pty/lib/eventEmitter2.d.ts
vendored
Normal file
13
web/package/node-pty/lib/eventEmitter2.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2019, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
import { IDisposable } from './types';
|
||||||
|
export interface IEvent<T> {
|
||||||
|
(listener: (e: T) => any): IDisposable;
|
||||||
|
}
|
||||||
|
export declare class EventEmitter2<T> {
|
||||||
|
private _listeners;
|
||||||
|
private _event?;
|
||||||
|
get event(): IEvent<T>;
|
||||||
|
fire(data: T): void;
|
||||||
|
}
|
||||||
40
web/package/node-pty/lib/eventEmitter2.js
Normal file
40
web/package/node-pty/lib/eventEmitter2.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2019, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.EventEmitter2 = void 0;
|
||||||
|
class EventEmitter2 {
|
||||||
|
constructor() {
|
||||||
|
this._listeners = [];
|
||||||
|
}
|
||||||
|
get event() {
|
||||||
|
if (!this._event) {
|
||||||
|
this._event = (listener) => {
|
||||||
|
this._listeners.push(listener);
|
||||||
|
const disposable = {
|
||||||
|
dispose: () => {
|
||||||
|
for (let i = 0; i < this._listeners.length; i++) {
|
||||||
|
if (this._listeners[i] === listener) {
|
||||||
|
this._listeners.splice(i, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return disposable;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this._event;
|
||||||
|
}
|
||||||
|
fire(data) {
|
||||||
|
const queue = [];
|
||||||
|
for (let i = 0; i < this._listeners.length; i++) {
|
||||||
|
queue.push(this._listeners[i]);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < queue.length; i++) {
|
||||||
|
queue[i].call(undefined, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.EventEmitter2 = EventEmitter2;
|
||||||
15
web/package/node-pty/lib/index.d.ts
vendored
Normal file
15
web/package/node-pty/lib/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Minimal PTY implementation without threading
|
||||||
|
* Vendored from node-pty, simplified to remove shared pipe architecture
|
||||||
|
*/
|
||||||
|
import { ITerminal, IPtyForkOptions, IWindowsPtyForkOptions } from './interfaces';
|
||||||
|
import { ArgvOrCommandLine } from './types';
|
||||||
|
/**
|
||||||
|
* Forks a process as a pseudoterminal.
|
||||||
|
*/
|
||||||
|
export declare function spawn(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions | IWindowsPtyForkOptions): ITerminal;
|
||||||
|
export declare const fork: typeof spawn;
|
||||||
|
export declare const createTerminal: typeof spawn;
|
||||||
|
export * from './interfaces';
|
||||||
|
export * from './types';
|
||||||
|
export type IPty = ITerminal;
|
||||||
41
web/package/node-pty/lib/index.js
Normal file
41
web/package/node-pty/lib/index.js
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* Minimal PTY implementation without threading
|
||||||
|
* Vendored from node-pty, simplified to remove shared pipe architecture
|
||||||
|
*/
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||||
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.createTerminal = exports.fork = void 0;
|
||||||
|
exports.spawn = spawn;
|
||||||
|
let terminalCtor;
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
terminalCtor = require('./windowsTerminal').WindowsTerminal;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
terminalCtor = require('./unixTerminal').UnixTerminal;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Forks a process as a pseudoterminal.
|
||||||
|
*/
|
||||||
|
function spawn(file, args, opt) {
|
||||||
|
return new terminalCtor(file, args, opt);
|
||||||
|
}
|
||||||
|
// Deprecated aliases
|
||||||
|
exports.fork = spawn;
|
||||||
|
exports.createTerminal = spawn;
|
||||||
|
// Re-export types
|
||||||
|
__exportStar(require("./interfaces"), exports);
|
||||||
|
__exportStar(require("./types"), exports);
|
||||||
120
web/package/node-pty/lib/interfaces.d.ts
vendored
Normal file
120
web/package/node-pty/lib/interfaces.d.ts
vendored
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2016, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
export interface IProcessEnv {
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
}
|
||||||
|
import type { IExitEvent } from './types';
|
||||||
|
import type { IEvent } from './eventEmitter2';
|
||||||
|
export interface ITerminal {
|
||||||
|
/**
|
||||||
|
* Gets the name of the process.
|
||||||
|
*/
|
||||||
|
process: string;
|
||||||
|
/**
|
||||||
|
* Gets the process ID.
|
||||||
|
*/
|
||||||
|
pid: number;
|
||||||
|
/**
|
||||||
|
* The data event.
|
||||||
|
*/
|
||||||
|
readonly onData: IEvent<string>;
|
||||||
|
/**
|
||||||
|
* The exit event.
|
||||||
|
*/
|
||||||
|
readonly onExit: IEvent<IExitEvent>;
|
||||||
|
/**
|
||||||
|
* Writes data to the socket.
|
||||||
|
* @param data The data to write.
|
||||||
|
*/
|
||||||
|
write(data: string): void;
|
||||||
|
/**
|
||||||
|
* Resize the pty.
|
||||||
|
* @param cols The number of columns.
|
||||||
|
* @param rows The number of rows.
|
||||||
|
*/
|
||||||
|
resize(cols: number, rows: number): void;
|
||||||
|
/**
|
||||||
|
* Clears the pty's internal representation of its buffer. This is a no-op
|
||||||
|
* unless on Windows/ConPTY.
|
||||||
|
*/
|
||||||
|
clear(): void;
|
||||||
|
/**
|
||||||
|
* Close, kill and destroy the socket.
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
/**
|
||||||
|
* Kill the pty.
|
||||||
|
* @param signal The signal to send, by default this is SIGHUP. This is not
|
||||||
|
* supported on Windows.
|
||||||
|
*/
|
||||||
|
kill(signal?: string): void;
|
||||||
|
/**
|
||||||
|
* Set the pty socket encoding.
|
||||||
|
*/
|
||||||
|
setEncoding(encoding: string | null): void;
|
||||||
|
/**
|
||||||
|
* Resume the pty socket.
|
||||||
|
*/
|
||||||
|
resume(): void;
|
||||||
|
/**
|
||||||
|
* Pause the pty socket.
|
||||||
|
*/
|
||||||
|
pause(): void;
|
||||||
|
/**
|
||||||
|
* Alias for ITerminal.on(eventName, listener).
|
||||||
|
*/
|
||||||
|
addListener(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
/**
|
||||||
|
* Adds the listener function to the end of the listeners array for the event
|
||||||
|
* named eventName.
|
||||||
|
* @param eventName The event name.
|
||||||
|
* @param listener The callback function
|
||||||
|
*/
|
||||||
|
on(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
/**
|
||||||
|
* Returns a copy of the array of listeners for the event named eventName.
|
||||||
|
*/
|
||||||
|
listeners(eventName: string): Function[];
|
||||||
|
/**
|
||||||
|
* Removes the specified listener from the listener array for the event named
|
||||||
|
* eventName.
|
||||||
|
*/
|
||||||
|
removeListener(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
/**
|
||||||
|
* Removes all listeners, or those of the specified eventName.
|
||||||
|
*/
|
||||||
|
removeAllListeners(eventName: string): void;
|
||||||
|
/**
|
||||||
|
* Adds a one time listener function for the event named eventName. The next
|
||||||
|
* time eventName is triggered, this listener is removed and then invoked.
|
||||||
|
*/
|
||||||
|
once(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
}
|
||||||
|
interface IBasePtyForkOptions {
|
||||||
|
name?: string;
|
||||||
|
cols?: number;
|
||||||
|
rows?: number;
|
||||||
|
cwd?: string;
|
||||||
|
env?: IProcessEnv;
|
||||||
|
encoding?: string | null;
|
||||||
|
handleFlowControl?: boolean;
|
||||||
|
flowControlPause?: string;
|
||||||
|
flowControlResume?: string;
|
||||||
|
}
|
||||||
|
export interface IPtyForkOptions extends IBasePtyForkOptions {
|
||||||
|
uid?: number;
|
||||||
|
gid?: number;
|
||||||
|
}
|
||||||
|
export interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
|
||||||
|
useConpty?: boolean;
|
||||||
|
useConptyDll?: boolean;
|
||||||
|
conptyInheritCursor?: boolean;
|
||||||
|
}
|
||||||
|
export interface IPtyOpenOptions {
|
||||||
|
cols?: number;
|
||||||
|
rows?: number;
|
||||||
|
encoding?: string | null;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
6
web/package/node-pty/lib/interfaces.js
Normal file
6
web/package/node-pty/lib/interfaces.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2016, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
66
web/package/node-pty/lib/terminal.d.ts
vendored
Normal file
66
web/package/node-pty/lib/terminal.d.ts
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
||||||
|
* Copyright (c) 2016, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
import { Socket } from 'net';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { ITerminal, IPtyForkOptions, IProcessEnv } from './interfaces';
|
||||||
|
import { IEvent } from './eventEmitter2';
|
||||||
|
import { IExitEvent } from './types';
|
||||||
|
export declare const DEFAULT_COLS: number;
|
||||||
|
export declare const DEFAULT_ROWS: number;
|
||||||
|
export declare abstract class Terminal implements ITerminal {
|
||||||
|
protected _socket: Socket;
|
||||||
|
protected _pid: number;
|
||||||
|
protected _fd: number;
|
||||||
|
protected _pty: any;
|
||||||
|
protected _file: string;
|
||||||
|
protected _name: string;
|
||||||
|
protected _cols: number;
|
||||||
|
protected _rows: number;
|
||||||
|
protected _readable: boolean;
|
||||||
|
protected _writable: boolean;
|
||||||
|
protected _internalee: EventEmitter;
|
||||||
|
private _flowControlPause;
|
||||||
|
private _flowControlResume;
|
||||||
|
handleFlowControl: boolean;
|
||||||
|
private _onData;
|
||||||
|
get onData(): IEvent<string>;
|
||||||
|
private _onExit;
|
||||||
|
get onExit(): IEvent<IExitEvent>;
|
||||||
|
get pid(): number;
|
||||||
|
get cols(): number;
|
||||||
|
get rows(): number;
|
||||||
|
constructor(opt?: IPtyForkOptions);
|
||||||
|
protected abstract _write(data: string): void;
|
||||||
|
write(data: string): void;
|
||||||
|
protected _forwardEvents(): void;
|
||||||
|
protected _checkType<T>(name: string, value: T | undefined, type: string, allowArray?: boolean): void;
|
||||||
|
/** See net.Socket.end */
|
||||||
|
end(data: string): void;
|
||||||
|
/** See stream.Readable.pipe */
|
||||||
|
pipe(dest: any, options: any): any;
|
||||||
|
/** See net.Socket.pause */
|
||||||
|
pause(): Socket;
|
||||||
|
/** See net.Socket.resume */
|
||||||
|
resume(): Socket;
|
||||||
|
/** See net.Socket.setEncoding */
|
||||||
|
setEncoding(encoding: string | null): void;
|
||||||
|
addListener(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
on(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
emit(eventName: string, ...args: any[]): any;
|
||||||
|
listeners(eventName: string): Function[];
|
||||||
|
removeListener(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
removeAllListeners(eventName: string): void;
|
||||||
|
once(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
abstract resize(cols: number, rows: number): void;
|
||||||
|
abstract clear(): void;
|
||||||
|
abstract destroy(): void;
|
||||||
|
abstract kill(signal?: string): void;
|
||||||
|
abstract get process(): string;
|
||||||
|
abstract get master(): Socket | undefined;
|
||||||
|
abstract get slave(): Socket | undefined;
|
||||||
|
protected _close(): void;
|
||||||
|
protected _parseEnv(env: IProcessEnv): string[];
|
||||||
|
}
|
||||||
162
web/package/node-pty/lib/terminal.js
Normal file
162
web/package/node-pty/lib/terminal.js
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
||||||
|
* Copyright (c) 2016, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.Terminal = exports.DEFAULT_ROWS = exports.DEFAULT_COLS = void 0;
|
||||||
|
const events_1 = require("events");
|
||||||
|
const eventEmitter2_1 = require("./eventEmitter2");
|
||||||
|
exports.DEFAULT_COLS = 80;
|
||||||
|
exports.DEFAULT_ROWS = 24;
|
||||||
|
/**
|
||||||
|
* Default messages to indicate PAUSE/RESUME for automatic flow control.
|
||||||
|
* To avoid conflicts with rebound XON/XOFF control codes (such as on-my-zsh),
|
||||||
|
* the sequences can be customized in `IPtyForkOptions`.
|
||||||
|
*/
|
||||||
|
const FLOW_CONTROL_PAUSE = '\x13'; // defaults to XOFF
|
||||||
|
const FLOW_CONTROL_RESUME = '\x11'; // defaults to XON
|
||||||
|
class Terminal {
|
||||||
|
get onData() { return this._onData.event; }
|
||||||
|
get onExit() { return this._onExit.event; }
|
||||||
|
get pid() { return this._pid; }
|
||||||
|
get cols() { return this._cols; }
|
||||||
|
get rows() { return this._rows; }
|
||||||
|
constructor(opt) {
|
||||||
|
this._pid = 0;
|
||||||
|
this._fd = 0;
|
||||||
|
this._cols = 0;
|
||||||
|
this._rows = 0;
|
||||||
|
this._readable = false;
|
||||||
|
this._writable = false;
|
||||||
|
this._onData = new eventEmitter2_1.EventEmitter2();
|
||||||
|
this._onExit = new eventEmitter2_1.EventEmitter2();
|
||||||
|
// for 'close'
|
||||||
|
this._internalee = new events_1.EventEmitter();
|
||||||
|
// setup flow control handling
|
||||||
|
this.handleFlowControl = !!(opt?.handleFlowControl);
|
||||||
|
this._flowControlPause = opt?.flowControlPause || FLOW_CONTROL_PAUSE;
|
||||||
|
this._flowControlResume = opt?.flowControlResume || FLOW_CONTROL_RESUME;
|
||||||
|
if (!opt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Do basic type checks here in case node-pty is being used within JavaScript. If the wrong
|
||||||
|
// types go through to the C++ side it can lead to hard to diagnose exceptions.
|
||||||
|
this._checkType('name', opt.name ? opt.name : undefined, 'string');
|
||||||
|
this._checkType('cols', opt.cols ? opt.cols : undefined, 'number');
|
||||||
|
this._checkType('rows', opt.rows ? opt.rows : undefined, 'number');
|
||||||
|
this._checkType('cwd', opt.cwd ? opt.cwd : undefined, 'string');
|
||||||
|
this._checkType('env', opt.env ? opt.env : undefined, 'object');
|
||||||
|
this._checkType('uid', opt.uid ? opt.uid : undefined, 'number');
|
||||||
|
this._checkType('gid', opt.gid ? opt.gid : undefined, 'number');
|
||||||
|
this._checkType('encoding', opt.encoding ? opt.encoding : undefined, 'string');
|
||||||
|
}
|
||||||
|
write(data) {
|
||||||
|
if (this.handleFlowControl) {
|
||||||
|
// PAUSE/RESUME messages are not forwarded to the pty
|
||||||
|
if (data === this._flowControlPause) {
|
||||||
|
this.pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data === this._flowControlResume) {
|
||||||
|
this.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// everything else goes to the real pty
|
||||||
|
this._write(data);
|
||||||
|
}
|
||||||
|
_forwardEvents() {
|
||||||
|
this.on('data', e => this._onData.fire(e));
|
||||||
|
this.on('exit', (exitCode, signal) => this._onExit.fire({ exitCode, signal }));
|
||||||
|
}
|
||||||
|
_checkType(name, value, type, allowArray = false) {
|
||||||
|
if (value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (allowArray) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v, i) => {
|
||||||
|
if (typeof v !== type) {
|
||||||
|
throw new Error(`${name}[${i}] must be a ${type} (not a ${typeof v})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof value !== type) {
|
||||||
|
throw new Error(`${name} must be a ${type} (not a ${typeof value})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** See net.Socket.end */
|
||||||
|
end(data) {
|
||||||
|
this._socket.end(data);
|
||||||
|
}
|
||||||
|
/** See stream.Readable.pipe */
|
||||||
|
pipe(dest, options) {
|
||||||
|
return this._socket.pipe(dest, options);
|
||||||
|
}
|
||||||
|
/** See net.Socket.pause */
|
||||||
|
pause() {
|
||||||
|
return this._socket.pause();
|
||||||
|
}
|
||||||
|
/** See net.Socket.resume */
|
||||||
|
resume() {
|
||||||
|
return this._socket.resume();
|
||||||
|
}
|
||||||
|
/** See net.Socket.setEncoding */
|
||||||
|
setEncoding(encoding) {
|
||||||
|
if (this._socket._decoder) {
|
||||||
|
delete this._socket._decoder;
|
||||||
|
}
|
||||||
|
if (encoding) {
|
||||||
|
this._socket.setEncoding(encoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addListener(eventName, listener) { this.on(eventName, listener); }
|
||||||
|
on(eventName, listener) {
|
||||||
|
if (eventName === 'close') {
|
||||||
|
this._internalee.on('close', listener);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._socket.on(eventName, listener);
|
||||||
|
}
|
||||||
|
emit(eventName, ...args) {
|
||||||
|
if (eventName === 'close') {
|
||||||
|
return this._internalee.emit.apply(this._internalee, arguments);
|
||||||
|
}
|
||||||
|
return this._socket.emit.apply(this._socket, arguments);
|
||||||
|
}
|
||||||
|
listeners(eventName) {
|
||||||
|
return this._socket.listeners(eventName);
|
||||||
|
}
|
||||||
|
removeListener(eventName, listener) {
|
||||||
|
this._socket.removeListener(eventName, listener);
|
||||||
|
}
|
||||||
|
removeAllListeners(eventName) {
|
||||||
|
this._socket.removeAllListeners(eventName);
|
||||||
|
}
|
||||||
|
once(eventName, listener) {
|
||||||
|
this._socket.once(eventName, listener);
|
||||||
|
}
|
||||||
|
_close() {
|
||||||
|
this._socket.readable = false;
|
||||||
|
this.write = () => { };
|
||||||
|
this.end = () => { };
|
||||||
|
this._writable = false;
|
||||||
|
this._readable = false;
|
||||||
|
}
|
||||||
|
_parseEnv(env) {
|
||||||
|
const keys = Object.keys(env || {});
|
||||||
|
const pairs = [];
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
if (keys[i] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pairs.push(keys[i] + '=' + env[keys[i]]);
|
||||||
|
}
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.Terminal = Terminal;
|
||||||
12
web/package/node-pty/lib/types.d.ts
vendored
Normal file
12
web/package/node-pty/lib/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
export type ArgvOrCommandLine = string[] | string;
|
||||||
|
export interface IExitEvent {
|
||||||
|
exitCode: number;
|
||||||
|
signal: number | undefined;
|
||||||
|
}
|
||||||
|
export interface IDisposable {
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
6
web/package/node-pty/lib/types.js
Normal file
6
web/package/node-pty/lib/types.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
43
web/package/node-pty/lib/unixTerminal.d.ts
vendored
Normal file
43
web/package/node-pty/lib/unixTerminal.d.ts
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
||||||
|
* Copyright (c) 2016, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
import * as net from 'net';
|
||||||
|
import { Terminal } from './terminal';
|
||||||
|
import { IPtyForkOptions, IPtyOpenOptions } from './interfaces';
|
||||||
|
import { ArgvOrCommandLine } from './types';
|
||||||
|
export declare class UnixTerminal extends Terminal {
|
||||||
|
protected _fd: number;
|
||||||
|
protected _pty: string;
|
||||||
|
protected _file: string;
|
||||||
|
protected _name: string;
|
||||||
|
protected _readable: boolean;
|
||||||
|
protected _writable: boolean;
|
||||||
|
private _boundClose;
|
||||||
|
private _emittedClose;
|
||||||
|
private _master;
|
||||||
|
private _slave;
|
||||||
|
get master(): net.Socket | undefined;
|
||||||
|
get slave(): net.Socket | undefined;
|
||||||
|
constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions);
|
||||||
|
protected _write(data: string): void;
|
||||||
|
get fd(): number;
|
||||||
|
get ptsName(): string;
|
||||||
|
/**
|
||||||
|
* openpty
|
||||||
|
*/
|
||||||
|
static open(opt: IPtyOpenOptions): UnixTerminal;
|
||||||
|
destroy(): void;
|
||||||
|
kill(signal?: string): void;
|
||||||
|
/**
|
||||||
|
* Gets the name of the process.
|
||||||
|
*/
|
||||||
|
get process(): string;
|
||||||
|
/**
|
||||||
|
* TTY
|
||||||
|
*/
|
||||||
|
resize(cols: number, rows: number): void;
|
||||||
|
clear(): void;
|
||||||
|
private _sanitizeEnv;
|
||||||
|
}
|
||||||
300
web/package/node-pty/lib/unixTerminal.js
Normal file
300
web/package/node-pty/lib/unixTerminal.js
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
"use strict";
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.UnixTerminal = void 0;
|
||||||
|
const path = __importStar(require("path"));
|
||||||
|
const tty = __importStar(require("tty"));
|
||||||
|
const terminal_1 = require("./terminal");
|
||||||
|
const utils_1 = require("./utils");
|
||||||
|
let pty;
|
||||||
|
let helperPath;
|
||||||
|
// Check if running in SEA (Single Executable Application) context
|
||||||
|
if (process.env.VIBETUNNEL_SEA) {
|
||||||
|
// In SEA mode, load native module using process.dlopen
|
||||||
|
const fs = require('fs');
|
||||||
|
const execDir = path.dirname(process.execPath);
|
||||||
|
const ptyPath = path.join(execDir, 'pty.node');
|
||||||
|
if (fs.existsSync(ptyPath)) {
|
||||||
|
const module = { exports: {} };
|
||||||
|
process.dlopen(module, ptyPath);
|
||||||
|
pty = module.exports;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`Could not find pty.node next to executable at: ${ptyPath}`);
|
||||||
|
}
|
||||||
|
// Set spawn-helper path for macOS only (Linux doesn't use it)
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
helperPath = path.join(execDir, 'spawn-helper');
|
||||||
|
if (!fs.existsSync(helperPath)) {
|
||||||
|
console.warn(`spawn-helper not found at ${helperPath}, PTY operations may fail`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Standard Node.js loading
|
||||||
|
try {
|
||||||
|
pty = require('../build/Release/pty.node');
|
||||||
|
helperPath = '../build/Release/spawn-helper';
|
||||||
|
}
|
||||||
|
catch (outerError) {
|
||||||
|
try {
|
||||||
|
pty = require('../build/Debug/pty.node');
|
||||||
|
helperPath = '../build/Debug/spawn-helper';
|
||||||
|
}
|
||||||
|
catch (innerError) {
|
||||||
|
console.error('innerError', innerError);
|
||||||
|
throw outerError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
helperPath = path.resolve(__dirname, helperPath);
|
||||||
|
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
|
||||||
|
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
|
||||||
|
}
|
||||||
|
const DEFAULT_FILE = 'sh';
|
||||||
|
const DEFAULT_NAME = 'xterm';
|
||||||
|
const DESTROY_SOCKET_TIMEOUT_MS = 200;
|
||||||
|
class UnixTerminal extends terminal_1.Terminal {
|
||||||
|
get master() { return this._master; }
|
||||||
|
get slave() { return this._slave; }
|
||||||
|
constructor(file, args, opt) {
|
||||||
|
super(opt);
|
||||||
|
this._boundClose = false;
|
||||||
|
this._emittedClose = false;
|
||||||
|
if (typeof args === 'string') {
|
||||||
|
throw new Error('args as a string is not supported on unix.');
|
||||||
|
}
|
||||||
|
// Initialize arguments
|
||||||
|
args = args || [];
|
||||||
|
file = file || DEFAULT_FILE;
|
||||||
|
opt = opt || {};
|
||||||
|
opt.env = opt.env || process.env;
|
||||||
|
this._cols = opt.cols || terminal_1.DEFAULT_COLS;
|
||||||
|
this._rows = opt.rows || terminal_1.DEFAULT_ROWS;
|
||||||
|
const uid = opt.uid ?? -1;
|
||||||
|
const gid = opt.gid ?? -1;
|
||||||
|
const env = (0, utils_1.assign)({}, opt.env);
|
||||||
|
if (opt.env === process.env) {
|
||||||
|
this._sanitizeEnv(env);
|
||||||
|
}
|
||||||
|
const cwd = opt.cwd || process.cwd();
|
||||||
|
env.PWD = cwd;
|
||||||
|
const name = opt.name || env.TERM || DEFAULT_NAME;
|
||||||
|
env.TERM = name;
|
||||||
|
const parsedEnv = this._parseEnv(env);
|
||||||
|
const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
|
||||||
|
const onexit = (code, signal) => {
|
||||||
|
// XXX Sometimes a data event is emitted after exit. Wait til socket is
|
||||||
|
// destroyed.
|
||||||
|
if (!this._emittedClose) {
|
||||||
|
if (this._boundClose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._boundClose = true;
|
||||||
|
// From macOS High Sierra 10.13.2 sometimes the socket never gets
|
||||||
|
// closed. A timeout is applied here to avoid the terminal never being
|
||||||
|
// destroyed when this occurs.
|
||||||
|
let timeout = setTimeout(() => {
|
||||||
|
timeout = null;
|
||||||
|
// Destroying the socket now will cause the close event to fire
|
||||||
|
this._socket.destroy();
|
||||||
|
}, DESTROY_SOCKET_TIMEOUT_MS);
|
||||||
|
this.once('close', () => {
|
||||||
|
if (timeout !== null) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
this.emit('exit', code, signal);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.emit('exit', code, signal);
|
||||||
|
};
|
||||||
|
// fork
|
||||||
|
const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), helperPath, onexit);
|
||||||
|
this._socket = new tty.ReadStream(term.fd);
|
||||||
|
if (encoding !== null) {
|
||||||
|
this._socket.setEncoding(encoding);
|
||||||
|
}
|
||||||
|
// setup
|
||||||
|
this._socket.on('error', (err) => {
|
||||||
|
// NOTE: fs.ReadStream gets EAGAIN twice at first:
|
||||||
|
if (err.code) {
|
||||||
|
if (~err.code.indexOf('EAGAIN')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// close
|
||||||
|
this._close();
|
||||||
|
// EIO on exit from fs.ReadStream:
|
||||||
|
if (!this._emittedClose) {
|
||||||
|
this._emittedClose = true;
|
||||||
|
this.emit('close');
|
||||||
|
}
|
||||||
|
// EIO, happens when someone closes our child process: the only process in
|
||||||
|
// the terminal.
|
||||||
|
// node < 0.6.14: errno 5
|
||||||
|
// node >= 0.6.14: read EIO
|
||||||
|
if (err.code) {
|
||||||
|
if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// throw anything else
|
||||||
|
if (this.listeners('error').length < 2) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._pid = term.pid;
|
||||||
|
this._fd = term.fd;
|
||||||
|
this._pty = term.pty;
|
||||||
|
this._file = file;
|
||||||
|
this._name = name;
|
||||||
|
this._readable = true;
|
||||||
|
this._writable = true;
|
||||||
|
this._socket.on('close', () => {
|
||||||
|
if (this._emittedClose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._emittedClose = true;
|
||||||
|
this._close();
|
||||||
|
this.emit('close');
|
||||||
|
});
|
||||||
|
this._forwardEvents();
|
||||||
|
}
|
||||||
|
_write(data) {
|
||||||
|
this._socket.write(data);
|
||||||
|
}
|
||||||
|
/* Accessors */
|
||||||
|
get fd() { return this._fd; }
|
||||||
|
get ptsName() { return this._pty; }
|
||||||
|
/**
|
||||||
|
* openpty
|
||||||
|
*/
|
||||||
|
static open(opt) {
|
||||||
|
const self = Object.create(UnixTerminal.prototype);
|
||||||
|
opt = opt || {};
|
||||||
|
if (arguments.length > 1) {
|
||||||
|
opt = {
|
||||||
|
cols: arguments[1],
|
||||||
|
rows: arguments[2]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const cols = opt.cols || terminal_1.DEFAULT_COLS;
|
||||||
|
const rows = opt.rows || terminal_1.DEFAULT_ROWS;
|
||||||
|
const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
|
||||||
|
// open
|
||||||
|
const term = pty.open(cols, rows);
|
||||||
|
self._master = new tty.ReadStream(term.master);
|
||||||
|
if (encoding !== null) {
|
||||||
|
self._master.setEncoding(encoding);
|
||||||
|
}
|
||||||
|
self._master.resume();
|
||||||
|
self._slave = new tty.ReadStream(term.slave);
|
||||||
|
if (encoding !== null) {
|
||||||
|
self._slave.setEncoding(encoding);
|
||||||
|
}
|
||||||
|
self._slave.resume();
|
||||||
|
self._socket = self._master;
|
||||||
|
self._pid = -1;
|
||||||
|
self._fd = term.master;
|
||||||
|
self._pty = term.pty;
|
||||||
|
self._file = process.argv[0] || 'node';
|
||||||
|
self._name = process.env.TERM || '';
|
||||||
|
self._readable = true;
|
||||||
|
self._writable = true;
|
||||||
|
self._socket.on('error', err => {
|
||||||
|
self._close();
|
||||||
|
if (self.listeners('error').length < 2) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self._socket.on('close', () => {
|
||||||
|
self._close();
|
||||||
|
});
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
this._close();
|
||||||
|
// Need to close the read stream so node stops reading a dead file
|
||||||
|
// descriptor. Then we can safely SIGHUP the shell.
|
||||||
|
this._socket.once('close', () => {
|
||||||
|
this.kill('SIGHUP');
|
||||||
|
});
|
||||||
|
this._socket.destroy();
|
||||||
|
}
|
||||||
|
kill(signal) {
|
||||||
|
try {
|
||||||
|
process.kill(this.pid, signal || 'SIGHUP');
|
||||||
|
}
|
||||||
|
catch (e) { /* swallow */ }
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Gets the name of the process.
|
||||||
|
*/
|
||||||
|
get process() {
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const title = pty.process(this._fd);
|
||||||
|
return (title !== 'kernel_task') ? title : this._file;
|
||||||
|
}
|
||||||
|
return pty.process(this._fd, this._pty) || this._file;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* TTY
|
||||||
|
*/
|
||||||
|
resize(cols, rows) {
|
||||||
|
if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) {
|
||||||
|
throw new Error('resizing must be done using positive cols and rows');
|
||||||
|
}
|
||||||
|
pty.resize(this._fd, cols, rows);
|
||||||
|
this._cols = cols;
|
||||||
|
this._rows = rows;
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
}
|
||||||
|
_sanitizeEnv(env) {
|
||||||
|
// Make sure we didn't start our server from inside tmux.
|
||||||
|
delete env['TMUX'];
|
||||||
|
delete env['TMUX_PANE'];
|
||||||
|
// Make sure we didn't start our server from inside screen.
|
||||||
|
// http://web.mit.edu/gnu/doc/html/screen_20.html
|
||||||
|
delete env['STY'];
|
||||||
|
delete env['WINDOW'];
|
||||||
|
// Delete some variables that might confuse our terminal.
|
||||||
|
delete env['WINDOWID'];
|
||||||
|
delete env['TERMCAP'];
|
||||||
|
delete env['COLUMNS'];
|
||||||
|
delete env['LINES'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.UnixTerminal = UnixTerminal;
|
||||||
5
web/package/node-pty/lib/utils.d.ts
vendored
Normal file
5
web/package/node-pty/lib/utils.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
export declare function assign(target: any, ...sources: any[]): any;
|
||||||
11
web/package/node-pty/lib/utils.js
Normal file
11
web/package/node-pty/lib/utils.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.assign = assign;
|
||||||
|
function assign(target, ...sources) {
|
||||||
|
sources.forEach(source => Object.keys(source).forEach(key => target[key] = source[key]));
|
||||||
|
return target;
|
||||||
|
}
|
||||||
34
web/package/node-pty/lib/windowsTerminal.d.ts
vendored
Normal file
34
web/package/node-pty/lib/windowsTerminal.d.ts
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* Simplified Windows terminal implementation without threading
|
||||||
|
* Removed ConoutSocketWorker and shared pipe architecture
|
||||||
|
*/
|
||||||
|
import { Socket } from 'net';
|
||||||
|
import { Terminal } from './terminal';
|
||||||
|
import { IPtyOpenOptions, IWindowsPtyForkOptions } from './interfaces';
|
||||||
|
import { ArgvOrCommandLine } from './types';
|
||||||
|
export declare class WindowsTerminal extends Terminal {
|
||||||
|
private _isReady;
|
||||||
|
protected _pid: number;
|
||||||
|
private _innerPid;
|
||||||
|
private _ptyNative;
|
||||||
|
protected _pty: number;
|
||||||
|
private _inSocket;
|
||||||
|
private _outSocket;
|
||||||
|
private _exitCode;
|
||||||
|
private _useConptyDll;
|
||||||
|
get master(): Socket | undefined;
|
||||||
|
get slave(): Socket | undefined;
|
||||||
|
constructor(file?: string, args?: ArgvOrCommandLine, opt?: IWindowsPtyForkOptions);
|
||||||
|
private _setupDirectSockets;
|
||||||
|
protected _write(data: string): void;
|
||||||
|
resize(cols: number, rows: number): void;
|
||||||
|
clear(): void;
|
||||||
|
kill(signal?: string): void;
|
||||||
|
protected _close(): void;
|
||||||
|
private _generatePipeName;
|
||||||
|
private _argsToCommandLine;
|
||||||
|
static open(options?: IPtyOpenOptions): void;
|
||||||
|
get process(): string;
|
||||||
|
get pid(): number;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
200
web/package/node-pty/lib/windowsTerminal.js
Normal file
200
web/package/node-pty/lib/windowsTerminal.js
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
"use strict";
|
||||||
|
/**
|
||||||
|
* Simplified Windows terminal implementation without threading
|
||||||
|
* Removed ConoutSocketWorker and shared pipe architecture
|
||||||
|
*/
|
||||||
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||||
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||||
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||||
|
}
|
||||||
|
Object.defineProperty(o, k2, desc);
|
||||||
|
}) : (function(o, m, k, k2) {
|
||||||
|
if (k2 === undefined) k2 = k;
|
||||||
|
o[k2] = m[k];
|
||||||
|
}));
|
||||||
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||||
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||||
|
}) : function(o, v) {
|
||||||
|
o["default"] = v;
|
||||||
|
});
|
||||||
|
var __importStar = (this && this.__importStar) || (function () {
|
||||||
|
var ownKeys = function(o) {
|
||||||
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||||
|
var ar = [];
|
||||||
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||||
|
return ar;
|
||||||
|
};
|
||||||
|
return ownKeys(o);
|
||||||
|
};
|
||||||
|
return function (mod) {
|
||||||
|
if (mod && mod.__esModule) return mod;
|
||||||
|
var result = {};
|
||||||
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||||
|
__setModuleDefault(result, mod);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.WindowsTerminal = void 0;
|
||||||
|
const fs = __importStar(require("fs"));
|
||||||
|
const net_1 = require("net");
|
||||||
|
const terminal_1 = require("./terminal");
|
||||||
|
const utils_1 = require("./utils");
|
||||||
|
const DEFAULT_FILE = 'cmd.exe';
|
||||||
|
const DEFAULT_NAME = 'Windows Shell';
|
||||||
|
let conptyNative;
|
||||||
|
class WindowsTerminal extends terminal_1.Terminal {
|
||||||
|
get master() { return this._outSocket; }
|
||||||
|
get slave() { return this._inSocket; }
|
||||||
|
constructor(file, args, opt) {
|
||||||
|
super(opt);
|
||||||
|
this._isReady = false;
|
||||||
|
this._pid = 0;
|
||||||
|
this._innerPid = 0;
|
||||||
|
this._useConptyDll = false;
|
||||||
|
// Load native module
|
||||||
|
if (!conptyNative) {
|
||||||
|
try {
|
||||||
|
conptyNative = require('../build/Release/conpty.node');
|
||||||
|
}
|
||||||
|
catch (outerError) {
|
||||||
|
try {
|
||||||
|
conptyNative = require('../build/Debug/conpty.node');
|
||||||
|
}
|
||||||
|
catch (innerError) {
|
||||||
|
throw outerError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._ptyNative = conptyNative;
|
||||||
|
// Initialize arguments
|
||||||
|
args = args || [];
|
||||||
|
file = file || DEFAULT_FILE;
|
||||||
|
opt = opt || {};
|
||||||
|
opt.env = opt.env || process.env;
|
||||||
|
const env = (0, utils_1.assign)({}, opt.env);
|
||||||
|
this._cols = opt.cols || terminal_1.DEFAULT_COLS;
|
||||||
|
this._rows = opt.rows || terminal_1.DEFAULT_ROWS;
|
||||||
|
const cwd = opt.cwd || process.cwd();
|
||||||
|
const parsedEnv = this._parseEnv(env);
|
||||||
|
// Compose command line
|
||||||
|
const commandLine = this._argsToCommandLine(file, args);
|
||||||
|
// Start ConPTY process
|
||||||
|
const pipeName = this._generatePipeName();
|
||||||
|
const term = this._ptyNative.startProcess(file, this._cols, this._rows, false, pipeName, false, this._useConptyDll);
|
||||||
|
this._pty = term.pty;
|
||||||
|
this._fd = term.fd;
|
||||||
|
// Create direct socket connections without worker threads
|
||||||
|
this._setupDirectSockets(term);
|
||||||
|
// Connect the process
|
||||||
|
const connect = this._ptyNative.connect(this._pty, commandLine, cwd, parsedEnv, this._useConptyDll, (exitCode) => {
|
||||||
|
this._exitCode = exitCode;
|
||||||
|
this.emit('exit', exitCode);
|
||||||
|
this._close();
|
||||||
|
});
|
||||||
|
this._innerPid = connect.pid;
|
||||||
|
this._pid = connect.pid;
|
||||||
|
this._file = file;
|
||||||
|
this._name = opt.name || env.TERM || DEFAULT_NAME;
|
||||||
|
this._readable = true;
|
||||||
|
this._writable = true;
|
||||||
|
this._forwardEvents();
|
||||||
|
}
|
||||||
|
_setupDirectSockets(term) {
|
||||||
|
// Setup output socket - read directly from conout
|
||||||
|
const outFd = fs.openSync(term.conout, 'r');
|
||||||
|
this._outSocket = new net_1.Socket({ fd: outFd, readable: true, writable: false });
|
||||||
|
this._outSocket.setEncoding('utf8');
|
||||||
|
this._socket = this._outSocket;
|
||||||
|
// Setup input socket - write directly to conin
|
||||||
|
const inFd = fs.openSync(term.conin, 'w');
|
||||||
|
this._inSocket = new net_1.Socket({ fd: inFd, readable: false, writable: true });
|
||||||
|
this._inSocket.setEncoding('utf8');
|
||||||
|
// Forward events directly
|
||||||
|
this._outSocket.on('data', (data) => {
|
||||||
|
if (!this._isReady) {
|
||||||
|
this._isReady = true;
|
||||||
|
}
|
||||||
|
this.emit('data', data);
|
||||||
|
});
|
||||||
|
this._outSocket.on('error', (err) => {
|
||||||
|
if (err.code && (err.code.includes('EPIPE') || err.code.includes('EIO'))) {
|
||||||
|
// Expected errors when process exits
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.emit('error', err);
|
||||||
|
});
|
||||||
|
this._outSocket.on('close', () => {
|
||||||
|
if (this._exitCode === undefined) {
|
||||||
|
this.emit('exit', 0);
|
||||||
|
}
|
||||||
|
this._close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_write(data) {
|
||||||
|
if (this._inSocket && this._inSocket.writable) {
|
||||||
|
this._inSocket.write(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resize(cols, rows) {
|
||||||
|
if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) {
|
||||||
|
throw new Error('resizing must be done using positive cols and rows');
|
||||||
|
}
|
||||||
|
if (this._exitCode !== undefined) {
|
||||||
|
throw new Error('Cannot resize a pty that has already exited');
|
||||||
|
}
|
||||||
|
this._cols = cols;
|
||||||
|
this._rows = rows;
|
||||||
|
this._ptyNative.resize(this._pty, cols, rows, this._useConptyDll);
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
this._ptyNative.clear(this._pty, this._useConptyDll);
|
||||||
|
}
|
||||||
|
kill(signal) {
|
||||||
|
this._close();
|
||||||
|
try {
|
||||||
|
process.kill(this._pid);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Ignore if process cannot be found
|
||||||
|
}
|
||||||
|
this._ptyNative.kill(this._pty, this._useConptyDll);
|
||||||
|
}
|
||||||
|
_close() {
|
||||||
|
if (this._inSocket) {
|
||||||
|
this._inSocket.destroy();
|
||||||
|
}
|
||||||
|
if (this._outSocket) {
|
||||||
|
this._outSocket.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_generatePipeName() {
|
||||||
|
return `\\\\.\\pipe\\conpty-${Date.now()}-${Math.random()}`;
|
||||||
|
}
|
||||||
|
_argsToCommandLine(file, args) {
|
||||||
|
if (typeof args === 'string') {
|
||||||
|
return `${file} ${args}`;
|
||||||
|
}
|
||||||
|
const argv = [file];
|
||||||
|
if (args) {
|
||||||
|
argv.push(...args);
|
||||||
|
}
|
||||||
|
return argv.map(arg => {
|
||||||
|
if (arg.includes(' ') || arg.includes('\t')) {
|
||||||
|
return `"${arg.replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}).join(' ');
|
||||||
|
}
|
||||||
|
static open(options) {
|
||||||
|
throw new Error('open() not supported on windows, use spawn() instead.');
|
||||||
|
}
|
||||||
|
get process() { return this._name; }
|
||||||
|
get pid() { return this._pid; }
|
||||||
|
destroy() {
|
||||||
|
this.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.WindowsTerminal = WindowsTerminal;
|
||||||
24
web/package/node-pty/package.json
Normal file
24
web/package/node-pty/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "node-pty",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Minimal PTY implementation without threading - vendored from node-pty",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && node-gyp rebuild",
|
||||||
|
"clean": "rimraf lib build",
|
||||||
|
"install": "node-gyp rebuild"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^7.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.0.3",
|
||||||
|
"node-gyp": "^11.0.0",
|
||||||
|
"prebuild": "^13.0.1",
|
||||||
|
"rimraf": "^5.0.5",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
},
|
||||||
|
"gypfile": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
48
web/package/node-pty/src/eventEmitter2.ts
Normal file
48
web/package/node-pty/src/eventEmitter2.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2019, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IDisposable } from './types';
|
||||||
|
|
||||||
|
interface IListener<T> {
|
||||||
|
(e: T): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEvent<T> {
|
||||||
|
(listener: (e: T) => any): IDisposable;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventEmitter2<T> {
|
||||||
|
private _listeners: IListener<T>[] = [];
|
||||||
|
private _event?: IEvent<T>;
|
||||||
|
|
||||||
|
public get event(): IEvent<T> {
|
||||||
|
if (!this._event) {
|
||||||
|
this._event = (listener: (e: T) => any) => {
|
||||||
|
this._listeners.push(listener);
|
||||||
|
const disposable = {
|
||||||
|
dispose: () => {
|
||||||
|
for (let i = 0; i < this._listeners.length; i++) {
|
||||||
|
if (this._listeners[i] === listener) {
|
||||||
|
this._listeners.splice(i, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return disposable;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this._event;
|
||||||
|
}
|
||||||
|
|
||||||
|
public fire(data: T): void {
|
||||||
|
const queue: IListener<T>[] = [];
|
||||||
|
for (let i = 0; i < this._listeners.length; i++) {
|
||||||
|
queue.push(this._listeners[i]);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < queue.length; i++) {
|
||||||
|
queue[i].call(undefined, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
web/package/node-pty/src/index.ts
Normal file
32
web/package/node-pty/src/index.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Minimal PTY implementation without threading
|
||||||
|
* Vendored from node-pty, simplified to remove shared pipe architecture
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ITerminal, IPtyForkOptions, IWindowsPtyForkOptions } from './interfaces';
|
||||||
|
import { ArgvOrCommandLine } from './types';
|
||||||
|
|
||||||
|
let terminalCtor: any;
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
terminalCtor = require('./windowsTerminal').WindowsTerminal;
|
||||||
|
} else {
|
||||||
|
terminalCtor = require('./unixTerminal').UnixTerminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forks a process as a pseudoterminal.
|
||||||
|
*/
|
||||||
|
export function spawn(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions | IWindowsPtyForkOptions): ITerminal {
|
||||||
|
return new terminalCtor(file, args, opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated aliases
|
||||||
|
export const fork = spawn;
|
||||||
|
export const createTerminal = spawn;
|
||||||
|
|
||||||
|
// Re-export types
|
||||||
|
export * from './interfaces';
|
||||||
|
export * from './types';
|
||||||
|
|
||||||
|
// Alias for compatibility
|
||||||
|
export type IPty = ITerminal;
|
||||||
143
web/package/node-pty/src/interfaces.ts
Normal file
143
web/package/node-pty/src/interfaces.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2016, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface IProcessEnv {
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
import type { IExitEvent } from './types';
|
||||||
|
import type { IEvent } from './eventEmitter2';
|
||||||
|
|
||||||
|
export interface ITerminal {
|
||||||
|
/**
|
||||||
|
* Gets the name of the process.
|
||||||
|
*/
|
||||||
|
process: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the process ID.
|
||||||
|
*/
|
||||||
|
pid: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data event.
|
||||||
|
*/
|
||||||
|
readonly onData: IEvent<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The exit event.
|
||||||
|
*/
|
||||||
|
readonly onExit: IEvent<IExitEvent>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes data to the socket.
|
||||||
|
* @param data The data to write.
|
||||||
|
*/
|
||||||
|
write(data: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize the pty.
|
||||||
|
* @param cols The number of columns.
|
||||||
|
* @param rows The number of rows.
|
||||||
|
*/
|
||||||
|
resize(cols: number, rows: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the pty's internal representation of its buffer. This is a no-op
|
||||||
|
* unless on Windows/ConPTY.
|
||||||
|
*/
|
||||||
|
clear(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close, kill and destroy the socket.
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kill the pty.
|
||||||
|
* @param signal The signal to send, by default this is SIGHUP. This is not
|
||||||
|
* supported on Windows.
|
||||||
|
*/
|
||||||
|
kill(signal?: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the pty socket encoding.
|
||||||
|
*/
|
||||||
|
setEncoding(encoding: string | null): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume the pty socket.
|
||||||
|
*/
|
||||||
|
resume(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause the pty socket.
|
||||||
|
*/
|
||||||
|
pause(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for ITerminal.on(eventName, listener).
|
||||||
|
*/
|
||||||
|
addListener(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the listener function to the end of the listeners array for the event
|
||||||
|
* named eventName.
|
||||||
|
* @param eventName The event name.
|
||||||
|
* @param listener The callback function
|
||||||
|
*/
|
||||||
|
on(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the array of listeners for the event named eventName.
|
||||||
|
*/
|
||||||
|
listeners(eventName: string): Function[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the specified listener from the listener array for the event named
|
||||||
|
* eventName.
|
||||||
|
*/
|
||||||
|
removeListener(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all listeners, or those of the specified eventName.
|
||||||
|
*/
|
||||||
|
removeAllListeners(eventName: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a one time listener function for the event named eventName. The next
|
||||||
|
* time eventName is triggered, this listener is removed and then invoked.
|
||||||
|
*/
|
||||||
|
once(eventName: string, listener: (...args: any[]) => any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IBasePtyForkOptions {
|
||||||
|
name?: string;
|
||||||
|
cols?: number;
|
||||||
|
rows?: number;
|
||||||
|
cwd?: string;
|
||||||
|
env?: IProcessEnv;
|
||||||
|
encoding?: string | null;
|
||||||
|
handleFlowControl?: boolean;
|
||||||
|
flowControlPause?: string;
|
||||||
|
flowControlResume?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPtyForkOptions extends IBasePtyForkOptions {
|
||||||
|
uid?: number;
|
||||||
|
gid?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWindowsPtyForkOptions extends IBasePtyForkOptions {
|
||||||
|
useConpty?: boolean;
|
||||||
|
useConptyDll?: boolean;
|
||||||
|
conptyInheritCursor?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPtyOpenOptions {
|
||||||
|
cols?: number;
|
||||||
|
rows?: number;
|
||||||
|
encoding?: string | null;
|
||||||
|
}
|
||||||
54
web/package/node-pty/src/native.d.ts
vendored
Normal file
54
web/package/node-pty/src/native.d.ts
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface IConptyNative {
|
||||||
|
startProcess(file: string, cols: number, rows: number, debug: boolean, pipeName: string, conptyInheritCursor: boolean, useConptyDll: boolean): IConptyProcess;
|
||||||
|
connect(ptyId: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExitCallback: (exitCode: number) => void): { pid: number };
|
||||||
|
resize(ptyId: number, cols: number, rows: number, useConptyDll: boolean): void;
|
||||||
|
clear(ptyId: number, useConptyDll: boolean): void;
|
||||||
|
kill(ptyId: number, useConptyDll: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWinptyNative {
|
||||||
|
startProcess(file: string, commandLine: string, env: string[], cwd: string, cols: number, rows: number, debug: boolean): IWinptyProcess;
|
||||||
|
resize(pid: number, cols: number, rows: number): void;
|
||||||
|
kill(pid: number, innerPid: number): void;
|
||||||
|
getProcessList(pid: number): number[];
|
||||||
|
getExitCode(innerPid: number): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUnixNative {
|
||||||
|
fork(file: string, args: string[], parsedEnv: string[], cwd: string, cols: number, rows: number, uid: number, gid: number, useUtf8: boolean, helperPath: string, onExitCallback: (code: number, signal: number) => void): IUnixProcess;
|
||||||
|
open(cols: number, rows: number): IUnixOpenProcess;
|
||||||
|
process(fd: number, pty?: string): string;
|
||||||
|
resize(fd: number, cols: number, rows: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IConptyProcess {
|
||||||
|
pty: number;
|
||||||
|
fd: number;
|
||||||
|
conin: string;
|
||||||
|
conout: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IWinptyProcess {
|
||||||
|
pty: number;
|
||||||
|
fd: number;
|
||||||
|
conin: string;
|
||||||
|
conout: string;
|
||||||
|
pid: number;
|
||||||
|
innerPid: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUnixProcess {
|
||||||
|
fd: number;
|
||||||
|
pid: number;
|
||||||
|
pty: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUnixOpenProcess {
|
||||||
|
master: number;
|
||||||
|
slave: number;
|
||||||
|
pty: string;
|
||||||
|
}
|
||||||
211
web/package/node-pty/src/terminal.ts
Normal file
211
web/package/node-pty/src/terminal.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
||||||
|
* Copyright (c) 2016, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Socket } from 'net';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { ITerminal, IPtyForkOptions, IProcessEnv } from './interfaces';
|
||||||
|
import { EventEmitter2, IEvent } from './eventEmitter2';
|
||||||
|
import { IExitEvent } from './types';
|
||||||
|
|
||||||
|
export const DEFAULT_COLS: number = 80;
|
||||||
|
export const DEFAULT_ROWS: number = 24;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default messages to indicate PAUSE/RESUME for automatic flow control.
|
||||||
|
* To avoid conflicts with rebound XON/XOFF control codes (such as on-my-zsh),
|
||||||
|
* the sequences can be customized in `IPtyForkOptions`.
|
||||||
|
*/
|
||||||
|
const FLOW_CONTROL_PAUSE = '\x13'; // defaults to XOFF
|
||||||
|
const FLOW_CONTROL_RESUME = '\x11'; // defaults to XON
|
||||||
|
|
||||||
|
export abstract class Terminal implements ITerminal {
|
||||||
|
protected _socket!: Socket; // HACK: This is unsafe
|
||||||
|
protected _pid: number = 0;
|
||||||
|
protected _fd: number = 0;
|
||||||
|
protected _pty: any;
|
||||||
|
|
||||||
|
protected _file!: string; // HACK: This is unsafe
|
||||||
|
protected _name!: string; // HACK: This is unsafe
|
||||||
|
protected _cols: number = 0;
|
||||||
|
protected _rows: number = 0;
|
||||||
|
|
||||||
|
protected _readable: boolean = false;
|
||||||
|
protected _writable: boolean = false;
|
||||||
|
|
||||||
|
protected _internalee: EventEmitter;
|
||||||
|
private _flowControlPause: string;
|
||||||
|
private _flowControlResume: string;
|
||||||
|
public handleFlowControl: boolean;
|
||||||
|
|
||||||
|
private _onData = new EventEmitter2<string>();
|
||||||
|
public get onData(): IEvent<string> { return this._onData.event; }
|
||||||
|
private _onExit = new EventEmitter2<IExitEvent>();
|
||||||
|
public get onExit(): IEvent<IExitEvent> { return this._onExit.event; }
|
||||||
|
|
||||||
|
public get pid(): number { return this._pid; }
|
||||||
|
public get cols(): number { return this._cols; }
|
||||||
|
public get rows(): number { return this._rows; }
|
||||||
|
|
||||||
|
constructor(opt?: IPtyForkOptions) {
|
||||||
|
// for 'close'
|
||||||
|
this._internalee = new EventEmitter();
|
||||||
|
|
||||||
|
// setup flow control handling
|
||||||
|
this.handleFlowControl = !!(opt?.handleFlowControl);
|
||||||
|
this._flowControlPause = opt?.flowControlPause || FLOW_CONTROL_PAUSE;
|
||||||
|
this._flowControlResume = opt?.flowControlResume || FLOW_CONTROL_RESUME;
|
||||||
|
|
||||||
|
if (!opt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do basic type checks here in case node-pty is being used within JavaScript. If the wrong
|
||||||
|
// types go through to the C++ side it can lead to hard to diagnose exceptions.
|
||||||
|
this._checkType('name', opt.name ? opt.name : undefined, 'string');
|
||||||
|
this._checkType('cols', opt.cols ? opt.cols : undefined, 'number');
|
||||||
|
this._checkType('rows', opt.rows ? opt.rows : undefined, 'number');
|
||||||
|
this._checkType('cwd', opt.cwd ? opt.cwd : undefined, 'string');
|
||||||
|
this._checkType('env', opt.env ? opt.env : undefined, 'object');
|
||||||
|
this._checkType('uid', opt.uid ? opt.uid : undefined, 'number');
|
||||||
|
this._checkType('gid', opt.gid ? opt.gid : undefined, 'number');
|
||||||
|
this._checkType('encoding', opt.encoding ? opt.encoding : undefined, 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract _write(data: string): void;
|
||||||
|
|
||||||
|
public write(data: string): void {
|
||||||
|
if (this.handleFlowControl) {
|
||||||
|
// PAUSE/RESUME messages are not forwarded to the pty
|
||||||
|
if (data === this._flowControlPause) {
|
||||||
|
this.pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data === this._flowControlResume) {
|
||||||
|
this.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// everything else goes to the real pty
|
||||||
|
this._write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _forwardEvents(): void {
|
||||||
|
this.on('data', e => this._onData.fire(e));
|
||||||
|
this.on('exit', (exitCode, signal) => this._onExit.fire({ exitCode, signal }));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _checkType<T>(name: string, value: T | undefined, type: string, allowArray: boolean = false): void {
|
||||||
|
if (value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (allowArray) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v, i) => {
|
||||||
|
if (typeof v !== type) {
|
||||||
|
throw new Error(`${name}[${i}] must be a ${type} (not a ${typeof v})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof value !== type) {
|
||||||
|
throw new Error(`${name} must be a ${type} (not a ${typeof value})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See net.Socket.end */
|
||||||
|
public end(data: string): void {
|
||||||
|
this._socket.end(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See stream.Readable.pipe */
|
||||||
|
public pipe(dest: any, options: any): any {
|
||||||
|
return this._socket.pipe(dest, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See net.Socket.pause */
|
||||||
|
public pause(): Socket {
|
||||||
|
return this._socket.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See net.Socket.resume */
|
||||||
|
public resume(): Socket {
|
||||||
|
return this._socket.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** See net.Socket.setEncoding */
|
||||||
|
public setEncoding(encoding: string | null): void {
|
||||||
|
if ((this._socket as any)._decoder) {
|
||||||
|
delete (this._socket as any)._decoder;
|
||||||
|
}
|
||||||
|
if (encoding) {
|
||||||
|
this._socket.setEncoding(encoding as BufferEncoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addListener(eventName: string, listener: (...args: any[]) => any): void { this.on(eventName, listener); }
|
||||||
|
public on(eventName: string, listener: (...args: any[]) => any): void {
|
||||||
|
if (eventName === 'close') {
|
||||||
|
this._internalee.on('close', listener);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._socket.on(eventName, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit(eventName: string, ...args: any[]): any {
|
||||||
|
if (eventName === 'close') {
|
||||||
|
return this._internalee.emit.apply(this._internalee, arguments as any);
|
||||||
|
}
|
||||||
|
return this._socket.emit.apply(this._socket, arguments as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
public listeners(eventName: string): Function[] {
|
||||||
|
return this._socket.listeners(eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeListener(eventName: string, listener: (...args: any[]) => any): void {
|
||||||
|
this._socket.removeListener(eventName, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeAllListeners(eventName: string): void {
|
||||||
|
this._socket.removeAllListeners(eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public once(eventName: string, listener: (...args: any[]) => any): void {
|
||||||
|
this._socket.once(eventName, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract resize(cols: number, rows: number): void;
|
||||||
|
public abstract clear(): void;
|
||||||
|
public abstract destroy(): void;
|
||||||
|
public abstract kill(signal?: string): void;
|
||||||
|
|
||||||
|
public abstract get process(): string;
|
||||||
|
public abstract get master(): Socket| undefined;
|
||||||
|
public abstract get slave(): Socket | undefined;
|
||||||
|
|
||||||
|
protected _close(): void {
|
||||||
|
this._socket.readable = false;
|
||||||
|
this.write = () => {};
|
||||||
|
this.end = () => {};
|
||||||
|
this._writable = false;
|
||||||
|
this._readable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _parseEnv(env: IProcessEnv): string[] {
|
||||||
|
const keys = Object.keys(env || {});
|
||||||
|
const pairs = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
if (keys[i] === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pairs.push(keys[i] + '=' + env[keys[i]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
web/package/node-pty/src/types.ts
Normal file
15
web/package/node-pty/src/types.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ArgvOrCommandLine = string[] | string;
|
||||||
|
|
||||||
|
export interface IExitEvent {
|
||||||
|
exitCode: number;
|
||||||
|
signal: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDisposable {
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
816
web/package/node-pty/src/unix/pty.cc
Normal file
816
web/package/node-pty/src/unix/pty.cc
Normal file
|
|
@ -0,0 +1,816 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
||||||
|
* Copyright (c) 2017, Daniel Imms (MIT License)
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define NODE_ADDON_API_DISABLE_DEPRECATED
|
||||||
|
#include <napi.h>
|
||||||
|
#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>
|
||||||
|
#include <termios.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
|
||||||
|
switch (status) {
|
||||||
|
case napi_closing:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case napi_queue_full:
|
||||||
|
Napi::Error::Fatal("SetupExitCallback", "Queue was full");
|
||||||
|
|
||||||
|
case napi_ok:
|
||||||
|
if (tsfn.Release() != napi_ok) {
|
||||||
|
Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.Release() failed");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.BlockingCall() failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reenable 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));
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
// Use TIOCPTYGNAME instead of ptsname() to avoid threading problems.
|
||||||
|
char slave_pty_name[128];
|
||||||
|
if (ioctl(master, TIOCPTYGNAME, slave_pty_name) != -1) {
|
||||||
|
obj.Set("pty", Napi::String::New(napiEnv, slave_pty_name));
|
||||||
|
} else {
|
||||||
|
obj.Set("pty", Napi::String::New(napiEnv, ""));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
obj.Set("pty", Napi::String::New(napiEnv, ptsname(master)));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
// Use TIOCPTYGNAME instead of ptsname() to avoid threading problems.
|
||||||
|
char slave_pty_name[128];
|
||||||
|
if (ioctl(master, TIOCPTYGNAME, slave_pty_name) != -1) {
|
||||||
|
obj.Set("pty", Napi::String::New(env, slave_pty_name));
|
||||||
|
} else {
|
||||||
|
obj.Set("pty", Napi::String::New(env, ""));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
obj.Set("pty", Napi::String::New(env, ptsname(master)));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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)
|
||||||
332
web/package/node-pty/src/unixTerminal.ts
Normal file
332
web/package/node-pty/src/unixTerminal.ts
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
|
||||||
|
* Copyright (c) 2016, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
import * as net from 'net';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as tty from 'tty';
|
||||||
|
import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from './terminal';
|
||||||
|
import { IProcessEnv, IPtyForkOptions, IPtyOpenOptions } from './interfaces';
|
||||||
|
import { ArgvOrCommandLine } from './types';
|
||||||
|
import { assign } from './utils';
|
||||||
|
|
||||||
|
let pty: IUnixNative;
|
||||||
|
let helperPath: string;
|
||||||
|
|
||||||
|
// Check if running in SEA (Single Executable Application) context
|
||||||
|
if (process.env.VIBETUNNEL_SEA) {
|
||||||
|
// In SEA mode, load native module using process.dlopen
|
||||||
|
const fs = require('fs');
|
||||||
|
const execDir = path.dirname(process.execPath);
|
||||||
|
const ptyPath = path.join(execDir, 'pty.node');
|
||||||
|
|
||||||
|
if (fs.existsSync(ptyPath)) {
|
||||||
|
const module = { exports: {} };
|
||||||
|
process.dlopen(module, ptyPath);
|
||||||
|
pty = module.exports as IUnixNative;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Could not find pty.node next to executable at: ${ptyPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set spawn-helper path for macOS only (Linux doesn't use it)
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
helperPath = path.join(execDir, 'spawn-helper');
|
||||||
|
if (!fs.existsSync(helperPath)) {
|
||||||
|
console.warn(`spawn-helper not found at ${helperPath}, PTY operations may fail`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Standard Node.js loading
|
||||||
|
try {
|
||||||
|
pty = require('../build/Release/pty.node');
|
||||||
|
helperPath = '../build/Release/spawn-helper';
|
||||||
|
} catch (outerError) {
|
||||||
|
try {
|
||||||
|
pty = require('../build/Debug/pty.node');
|
||||||
|
helperPath = '../build/Debug/spawn-helper';
|
||||||
|
} catch (innerError) {
|
||||||
|
console.error('innerError', innerError);
|
||||||
|
throw outerError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
helperPath = path.resolve(__dirname, helperPath);
|
||||||
|
helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
|
||||||
|
helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_FILE = 'sh';
|
||||||
|
const DEFAULT_NAME = 'xterm';
|
||||||
|
const DESTROY_SOCKET_TIMEOUT_MS = 200;
|
||||||
|
|
||||||
|
export class UnixTerminal extends Terminal {
|
||||||
|
protected _fd: number;
|
||||||
|
protected _pty: string;
|
||||||
|
|
||||||
|
protected _file: string;
|
||||||
|
protected _name: string;
|
||||||
|
|
||||||
|
protected _readable: boolean;
|
||||||
|
protected _writable: boolean;
|
||||||
|
|
||||||
|
private _boundClose: boolean = false;
|
||||||
|
private _emittedClose: boolean = false;
|
||||||
|
private _master: net.Socket | undefined;
|
||||||
|
private _slave: net.Socket | undefined;
|
||||||
|
|
||||||
|
public get master(): net.Socket | undefined { return this._master; }
|
||||||
|
public get slave(): net.Socket | undefined { return this._slave; }
|
||||||
|
|
||||||
|
constructor(file?: string, args?: ArgvOrCommandLine, opt?: IPtyForkOptions) {
|
||||||
|
super(opt);
|
||||||
|
|
||||||
|
if (typeof args === 'string') {
|
||||||
|
throw new Error('args as a string is not supported on unix.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize arguments
|
||||||
|
args = args || [];
|
||||||
|
file = file || DEFAULT_FILE;
|
||||||
|
opt = opt || {};
|
||||||
|
opt.env = opt.env || process.env;
|
||||||
|
|
||||||
|
this._cols = opt.cols || DEFAULT_COLS;
|
||||||
|
this._rows = opt.rows || DEFAULT_ROWS;
|
||||||
|
const uid = opt.uid ?? -1;
|
||||||
|
const gid = opt.gid ?? -1;
|
||||||
|
const env: IProcessEnv = assign({}, opt.env);
|
||||||
|
|
||||||
|
if (opt.env === process.env) {
|
||||||
|
this._sanitizeEnv(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cwd = opt.cwd || process.cwd();
|
||||||
|
env.PWD = cwd;
|
||||||
|
const name = opt.name || env.TERM || DEFAULT_NAME;
|
||||||
|
env.TERM = name;
|
||||||
|
const parsedEnv = this._parseEnv(env);
|
||||||
|
|
||||||
|
const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
|
||||||
|
|
||||||
|
const onexit = (code: number, signal: number): void => {
|
||||||
|
// XXX Sometimes a data event is emitted after exit. Wait til socket is
|
||||||
|
// destroyed.
|
||||||
|
if (!this._emittedClose) {
|
||||||
|
if (this._boundClose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._boundClose = true;
|
||||||
|
// From macOS High Sierra 10.13.2 sometimes the socket never gets
|
||||||
|
// closed. A timeout is applied here to avoid the terminal never being
|
||||||
|
// destroyed when this occurs.
|
||||||
|
let timeout: NodeJS.Timeout | null = setTimeout(() => {
|
||||||
|
timeout = null;
|
||||||
|
// Destroying the socket now will cause the close event to fire
|
||||||
|
this._socket.destroy();
|
||||||
|
}, DESTROY_SOCKET_TIMEOUT_MS);
|
||||||
|
this.once('close', () => {
|
||||||
|
if (timeout !== null) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
this.emit('exit', code, signal);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.emit('exit', code, signal);
|
||||||
|
};
|
||||||
|
|
||||||
|
// fork
|
||||||
|
const term = pty.fork(file, args, parsedEnv, cwd, this._cols, this._rows, uid, gid, (encoding === 'utf8'), helperPath, onexit);
|
||||||
|
|
||||||
|
this._socket = new tty.ReadStream(term.fd);
|
||||||
|
if (encoding !== null) {
|
||||||
|
this._socket.setEncoding(encoding as BufferEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup
|
||||||
|
this._socket.on('error', (err: any) => {
|
||||||
|
// NOTE: fs.ReadStream gets EAGAIN twice at first:
|
||||||
|
if (err.code) {
|
||||||
|
if (~err.code.indexOf('EAGAIN')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// close
|
||||||
|
this._close();
|
||||||
|
// EIO on exit from fs.ReadStream:
|
||||||
|
if (!this._emittedClose) {
|
||||||
|
this._emittedClose = true;
|
||||||
|
this.emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
// EIO, happens when someone closes our child process: the only process in
|
||||||
|
// the terminal.
|
||||||
|
// node < 0.6.14: errno 5
|
||||||
|
// node >= 0.6.14: read EIO
|
||||||
|
if (err.code) {
|
||||||
|
if (~err.code.indexOf('errno 5') || ~err.code.indexOf('EIO')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// throw anything else
|
||||||
|
if (this.listeners('error').length < 2) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._pid = term.pid;
|
||||||
|
this._fd = term.fd;
|
||||||
|
this._pty = term.pty;
|
||||||
|
|
||||||
|
this._file = file;
|
||||||
|
this._name = name;
|
||||||
|
|
||||||
|
this._readable = true;
|
||||||
|
this._writable = true;
|
||||||
|
|
||||||
|
this._socket.on('close', () => {
|
||||||
|
if (this._emittedClose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._emittedClose = true;
|
||||||
|
this._close();
|
||||||
|
this.emit('close');
|
||||||
|
});
|
||||||
|
|
||||||
|
this._forwardEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _write(data: string): void {
|
||||||
|
this._socket.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessors */
|
||||||
|
get fd(): number { return this._fd; }
|
||||||
|
get ptsName(): string { return this._pty; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* openpty
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static open(opt: IPtyOpenOptions): UnixTerminal {
|
||||||
|
const self: UnixTerminal = Object.create(UnixTerminal.prototype);
|
||||||
|
opt = opt || {};
|
||||||
|
|
||||||
|
if (arguments.length > 1) {
|
||||||
|
opt = {
|
||||||
|
cols: arguments[1],
|
||||||
|
rows: arguments[2]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cols = opt.cols || DEFAULT_COLS;
|
||||||
|
const rows = opt.rows || DEFAULT_ROWS;
|
||||||
|
const encoding = (opt.encoding === undefined ? 'utf8' : opt.encoding);
|
||||||
|
|
||||||
|
// open
|
||||||
|
const term: IUnixOpenProcess = pty.open(cols, rows);
|
||||||
|
|
||||||
|
self._master = new tty.ReadStream(term.master);
|
||||||
|
if (encoding !== null) {
|
||||||
|
self._master.setEncoding(encoding as BufferEncoding);
|
||||||
|
}
|
||||||
|
self._master.resume();
|
||||||
|
|
||||||
|
self._slave = new tty.ReadStream(term.slave);
|
||||||
|
if (encoding !== null) {
|
||||||
|
self._slave.setEncoding(encoding as BufferEncoding);
|
||||||
|
}
|
||||||
|
self._slave.resume();
|
||||||
|
|
||||||
|
self._socket = self._master;
|
||||||
|
self._pid = -1;
|
||||||
|
self._fd = term.master;
|
||||||
|
self._pty = term.pty;
|
||||||
|
|
||||||
|
self._file = process.argv[0] || 'node';
|
||||||
|
self._name = process.env.TERM || '';
|
||||||
|
|
||||||
|
self._readable = true;
|
||||||
|
self._writable = true;
|
||||||
|
|
||||||
|
self._socket.on('error', err => {
|
||||||
|
self._close();
|
||||||
|
if (self.listeners('error').length < 2) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self._socket.on('close', () => {
|
||||||
|
self._close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this._close();
|
||||||
|
|
||||||
|
// Need to close the read stream so node stops reading a dead file
|
||||||
|
// descriptor. Then we can safely SIGHUP the shell.
|
||||||
|
this._socket.once('close', () => {
|
||||||
|
this.kill('SIGHUP');
|
||||||
|
});
|
||||||
|
|
||||||
|
this._socket.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public kill(signal?: string): void {
|
||||||
|
try {
|
||||||
|
process.kill(this.pid, signal || 'SIGHUP');
|
||||||
|
} catch (e) { /* swallow */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the process.
|
||||||
|
*/
|
||||||
|
public get process(): string {
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const title = pty.process(this._fd);
|
||||||
|
return (title !== 'kernel_task' ) ? title : this._file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pty.process(this._fd, this._pty) || this._file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TTY
|
||||||
|
*/
|
||||||
|
|
||||||
|
public resize(cols: number, rows: number): void {
|
||||||
|
if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) {
|
||||||
|
throw new Error('resizing must be done using positive cols and rows');
|
||||||
|
}
|
||||||
|
pty.resize(this._fd, cols, rows);
|
||||||
|
this._cols = cols;
|
||||||
|
this._rows = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private _sanitizeEnv(env: IProcessEnv): void {
|
||||||
|
// Make sure we didn't start our server from inside tmux.
|
||||||
|
delete env['TMUX'];
|
||||||
|
delete env['TMUX_PANE'];
|
||||||
|
|
||||||
|
// Make sure we didn't start our server from inside screen.
|
||||||
|
// http://web.mit.edu/gnu/doc/html/screen_20.html
|
||||||
|
delete env['STY'];
|
||||||
|
delete env['WINDOW'];
|
||||||
|
|
||||||
|
// Delete some variables that might confuse our terminal.
|
||||||
|
delete env['WINDOWID'];
|
||||||
|
delete env['TERMCAP'];
|
||||||
|
delete env['COLUMNS'];
|
||||||
|
delete env['LINES'];
|
||||||
|
}
|
||||||
|
}
|
||||||
9
web/package/node-pty/src/utils.ts
Normal file
9
web/package/node-pty/src/utils.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2017, Daniel Imms (MIT License).
|
||||||
|
* Copyright (c) 2018, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function assign(target: any, ...sources: any[]): any {
|
||||||
|
sources.forEach(source => Object.keys(source).forEach(key => target[key] = source[key]));
|
||||||
|
return target;
|
||||||
|
}
|
||||||
583
web/package/node-pty/src/win/conpty.cc
Normal file
583
web/package/node-pty/src/win/conpty.cc
Normal file
|
|
@ -0,0 +1,583 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define _WIN32_WINNT 0x600
|
||||||
|
|
||||||
|
#define NODE_ADDON_API_DISABLE_DEPRECATED
|
||||||
|
#include <node_api.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <Shlwapi.h> // PathCombine, PathIsRelative
|
||||||
|
#include <sstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <strsafe.h>
|
||||||
|
#include "path_util.h"
|
||||||
|
#include "conpty.h"
|
||||||
|
|
||||||
|
// 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 HRESULT (__stdcall *PFNCLEARPSEUDOCONSOLE)(HPCON hpc);
|
||||||
|
typedef void (__stdcall *PFNCLOSEPSEUDOCONSOLE)(HPCON hpc);
|
||||||
|
typedef void (__stdcall *PFNRELEASEPSEUDOCONSOLE)(HPCON hpc);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct pty_baton {
|
||||||
|
int id;
|
||||||
|
HANDLE hIn;
|
||||||
|
HANDLE hOut;
|
||||||
|
HPCON hpc;
|
||||||
|
|
||||||
|
HANDLE hShell;
|
||||||
|
|
||||||
|
pty_baton(int _id, HANDLE _hIn, HANDLE _hOut, HPCON _hpc) : id(_id), hIn(_hIn), hOut(_hOut), hpc(_hpc) {};
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::vector<std::unique_ptr<pty_baton>> ptyHandles;
|
||||||
|
static volatile LONG ptyCounter;
|
||||||
|
|
||||||
|
static pty_baton* get_pty_baton(int id) {
|
||||||
|
auto it = std::find_if(ptyHandles.begin(), ptyHandles.end(), [id](const auto& ptyHandle) {
|
||||||
|
return ptyHandle->id == id;
|
||||||
|
});
|
||||||
|
if (it != ptyHandles.end()) {
|
||||||
|
return it->get();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool remove_pty_baton(int id) {
|
||||||
|
auto it = std::remove_if(ptyHandles.begin(), ptyHandles.end(), [id](const auto& ptyHandle) {
|
||||||
|
return ptyHandle->id == id;
|
||||||
|
});
|
||||||
|
if (it != ptyHandles.end()) {
|
||||||
|
ptyHandles.erase(it);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExitEvent {
|
||||||
|
int exit_code = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) {
|
||||||
|
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), baton] {
|
||||||
|
auto callback = [](Napi::Env env, Napi::Function cb, ExitEvent *exit_event) {
|
||||||
|
cb.Call({Napi::Number::New(env, exit_event->exit_code)});
|
||||||
|
delete exit_event;
|
||||||
|
};
|
||||||
|
|
||||||
|
ExitEvent *exit_event = new ExitEvent;
|
||||||
|
// Wait for process to complete.
|
||||||
|
WaitForSingleObject(baton->hShell, INFINITE);
|
||||||
|
// Get process exit code.
|
||||||
|
GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code));
|
||||||
|
// Clean up handles
|
||||||
|
CloseHandle(baton->hShell);
|
||||||
|
assert(remove_pty_baton(baton->id));
|
||||||
|
|
||||||
|
auto status = tsfn.BlockingCall(exit_event, callback); // In main thread
|
||||||
|
switch (status) {
|
||||||
|
case napi_closing:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case napi_queue_full:
|
||||||
|
Napi::Error::Fatal("SetupExitCallback", "Queue was full");
|
||||||
|
|
||||||
|
case napi_ok:
|
||||||
|
if (tsfn.Release() != napi_ok) {
|
||||||
|
Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.Release() failed");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Napi::Error::Fatal("SetupExitCallback", "ThreadSafeFunction.BlockingCall() failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Napi::Error errorWithCode(const Napi::CallbackInfo& info, const char* text) {
|
||||||
|
std::stringstream errorText;
|
||||||
|
errorText << text;
|
||||||
|
errorText << ", error code: " << GetLastError();
|
||||||
|
return Napi::Error::New(info.Env(), errorText.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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=*/128 * 1024,
|
||||||
|
/*nInBufferSize=*/128 * 1024,
|
||||||
|
/*nDefaultTimeOut=*/30000,
|
||||||
|
&sa);
|
||||||
|
|
||||||
|
return *hServer != INVALID_HANDLE_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE LoadConptyDll(const Napi::CallbackInfo& info,
|
||||||
|
const bool useConptyDll)
|
||||||
|
{
|
||||||
|
if (!useConptyDll) {
|
||||||
|
return LoadLibraryExW(L"kernel32.dll", 0, 0);
|
||||||
|
}
|
||||||
|
wchar_t currentDir[MAX_PATH];
|
||||||
|
HMODULE hModule = GetModuleHandleA("conpty.node");
|
||||||
|
if (hModule == NULL) {
|
||||||
|
throw errorWithCode(info, "Failed to get conpty.node module handle");
|
||||||
|
}
|
||||||
|
DWORD result = GetModuleFileNameW(hModule, currentDir, MAX_PATH);
|
||||||
|
if (result == 0) {
|
||||||
|
throw errorWithCode(info, "Failed to get conpty.node module file name");
|
||||||
|
}
|
||||||
|
PathRemoveFileSpecW(currentDir);
|
||||||
|
wchar_t conptyDllPath[MAX_PATH];
|
||||||
|
PathCombineW(conptyDllPath, currentDir, L"conpty\\conpty.dll");
|
||||||
|
if (!path_util::file_exists(conptyDllPath)) {
|
||||||
|
std::wstring errorMessage = L"Cannot find conpty.dll at " + std::wstring(conptyDllPath);
|
||||||
|
std::string errorMessageStr = path_util::wstring_to_string(errorMessage);
|
||||||
|
throw errorWithCode(info, errorMessageStr.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoadLibraryW(conptyDllPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT CreateNamedPipesAndPseudoConsole(const Napi::CallbackInfo& info,
|
||||||
|
COORD size,
|
||||||
|
DWORD dwFlags,
|
||||||
|
HANDLE *phInput,
|
||||||
|
HANDLE *phOutput,
|
||||||
|
HPCON* phPC,
|
||||||
|
std::wstring& inName,
|
||||||
|
std::wstring& outName,
|
||||||
|
const std::wstring& pipeName,
|
||||||
|
const bool useConptyDll)
|
||||||
|
{
|
||||||
|
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
bool fLoadedDll = hLibrary != nullptr;
|
||||||
|
if (fLoadedDll)
|
||||||
|
{
|
||||||
|
PFNCREATEPSEUDOCONSOLE const pfnCreate = (PFNCREATEPSEUDOCONSOLE)GetProcAddress(
|
||||||
|
(HMODULE)hLibrary,
|
||||||
|
useConptyDll ? "ConptyCreatePseudoConsole" : "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());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw errorWithCode(info, "Failed to load conpty.dll");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failed to find kernel32. This is realy 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 Napi::Value PtyStartProcess(const Napi::CallbackInfo& info) {
|
||||||
|
Napi::Env env(info.Env());
|
||||||
|
Napi::HandleScope scope(env);
|
||||||
|
|
||||||
|
Napi::Object marshal;
|
||||||
|
std::wstring inName, outName;
|
||||||
|
BOOL fSuccess = FALSE;
|
||||||
|
std::unique_ptr<wchar_t[]> mutableCommandline;
|
||||||
|
PROCESS_INFORMATION _piClient{};
|
||||||
|
|
||||||
|
if (info.Length() != 7 ||
|
||||||
|
!info[0].IsString() ||
|
||||||
|
!info[1].IsNumber() ||
|
||||||
|
!info[2].IsNumber() ||
|
||||||
|
!info[3].IsBoolean() ||
|
||||||
|
!info[4].IsString() ||
|
||||||
|
!info[5].IsBoolean() ||
|
||||||
|
!info[6].IsBoolean()) {
|
||||||
|
throw Napi::Error::New(env, "Usage: pty.startProcess(file, cols, rows, debug, pipeName, inheritCursor, useConptyDll)");
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::wstring filename(path_util::to_wstring(info[0].As<Napi::String>()));
|
||||||
|
const SHORT cols = static_cast<SHORT>(info[1].As<Napi::Number>().Uint32Value());
|
||||||
|
const SHORT rows = static_cast<SHORT>(info[2].As<Napi::Number>().Uint32Value());
|
||||||
|
const bool debug = info[3].As<Napi::Boolean>().Value();
|
||||||
|
const std::wstring pipeName(path_util::to_wstring(info[4].As<Napi::String>()));
|
||||||
|
const bool inheritCursor = info[5].As<Napi::Boolean>().Value();
|
||||||
|
const bool useConptyDll = info[6].As<Napi::Boolean>().Value();
|
||||||
|
|
||||||
|
// use environment 'Path' variable to determine location of
|
||||||
|
// the relative path that we have recieved (e.g cmd.exe)
|
||||||
|
std::wstring shellpath;
|
||||||
|
if (::PathIsRelativeW(filename.c_str())) {
|
||||||
|
shellpath = path_util::get_shell_path(filename.c_str());
|
||||||
|
} else {
|
||||||
|
shellpath = filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shellpath.empty() || !path_util::file_exists(shellpath)) {
|
||||||
|
std::string why;
|
||||||
|
why += "File not found: ";
|
||||||
|
why += path_util::wstring_to_string(shellpath);
|
||||||
|
throw Napi::Error::New(env, why);
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE hIn, hOut;
|
||||||
|
HPCON hpc;
|
||||||
|
HRESULT hr = CreateNamedPipesAndPseudoConsole(info, {cols, rows}, inheritCursor ? 1/*PSEUDOCONSOLE_INHERIT_CURSOR*/ : 0, &hIn, &hOut, &hpc, inName, outName, pipeName, useConptyDll);
|
||||||
|
|
||||||
|
// Restore default handling of ctrl+c
|
||||||
|
SetConsoleCtrlHandler(NULL, FALSE);
|
||||||
|
|
||||||
|
// Set return values
|
||||||
|
marshal = Napi::Object::New(env);
|
||||||
|
|
||||||
|
if (SUCCEEDED(hr)) {
|
||||||
|
// We were able to instantiate a conpty
|
||||||
|
const int ptyId = InterlockedIncrement(&ptyCounter);
|
||||||
|
marshal.Set("pty", Napi::Number::New(env, ptyId));
|
||||||
|
ptyHandles.emplace_back(
|
||||||
|
std::make_unique<pty_baton>(ptyId, hIn, hOut, hpc));
|
||||||
|
} else {
|
||||||
|
throw Napi::Error::New(env, "Cannot launch conpty");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string inNameStr = path_util::wstring_to_string(inName);
|
||||||
|
if (inNameStr.empty()) {
|
||||||
|
throw Napi::Error::New(env, "Failed to initialize conpty conin");
|
||||||
|
}
|
||||||
|
std::string outNameStr = path_util::wstring_to_string(outName);
|
||||||
|
if (outNameStr.empty()) {
|
||||||
|
throw Napi::Error::New(env, "Failed to initialize conpty conout");
|
||||||
|
}
|
||||||
|
|
||||||
|
marshal.Set("fd", Napi::Number::New(env, -1));
|
||||||
|
marshal.Set("conin", Napi::String::New(env, inNameStr));
|
||||||
|
marshal.Set("conout", Napi::String::New(env, outNameStr));
|
||||||
|
return marshal;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
|
||||||
|
Napi::Env env(info.Env());
|
||||||
|
Napi::HandleScope scope(env);
|
||||||
|
|
||||||
|
// 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() != 6 ||
|
||||||
|
!info[0].IsNumber() ||
|
||||||
|
!info[1].IsString() ||
|
||||||
|
!info[2].IsString() ||
|
||||||
|
!info[3].IsArray() ||
|
||||||
|
!info[4].IsBoolean() ||
|
||||||
|
!info[5].IsFunction()) {
|
||||||
|
throw Napi::Error::New(env, "Usage: pty.connect(id, cmdline, cwd, env, useConptyDll, exitCallback)");
|
||||||
|
}
|
||||||
|
|
||||||
|
const int id = info[0].As<Napi::Number>().Int32Value();
|
||||||
|
const std::wstring cmdline(path_util::to_wstring(info[1].As<Napi::String>()));
|
||||||
|
const std::wstring cwd(path_util::to_wstring(info[2].As<Napi::String>()));
|
||||||
|
const Napi::Array envValues = info[3].As<Napi::Array>();
|
||||||
|
const bool useConptyDll = info[4].As<Napi::Boolean>().Value();
|
||||||
|
Napi::Function exitCallback = info[5].As<Napi::Function>();
|
||||||
|
|
||||||
|
// Fetch pty handle from ID and start process
|
||||||
|
pty_baton* handle = get_pty_baton(id);
|
||||||
|
if (!handle) {
|
||||||
|
throw Napi::Error::New(env, "Invalid pty handle");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 envStr;
|
||||||
|
if (!envValues.IsEmpty()) {
|
||||||
|
std::wstring envBlock;
|
||||||
|
for(uint32_t i = 0; i < envValues.Length(); i++) {
|
||||||
|
envBlock += path_util::to_wstring(envValues.Get(i).As<Napi::String>());
|
||||||
|
envBlock += L'\0';
|
||||||
|
}
|
||||||
|
envBlock += L'\0';
|
||||||
|
envStr = std::move(envBlock);
|
||||||
|
}
|
||||||
|
std::vector<wchar_t> envV(envStr.cbegin(), envStr.cend());
|
||||||
|
LPWSTR envArg = envV.empty() ? nullptr : envV.data();
|
||||||
|
|
||||||
|
ConnectNamedPipe(handle->hIn, nullptr);
|
||||||
|
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) {
|
||||||
|
throw errorWithCode(info, "InitializeProcThreadAttributeList failed");
|
||||||
|
}
|
||||||
|
fSuccess = UpdateProcThreadAttribute(siEx.lpAttributeList,
|
||||||
|
0,
|
||||||
|
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
||||||
|
handle->hpc,
|
||||||
|
sizeof(HPCON),
|
||||||
|
NULL,
|
||||||
|
NULL);
|
||||||
|
if (!fSuccess) {
|
||||||
|
throw errorWithCode(info, "UpdateProcThreadAttribute failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw errorWithCode(info, "Cannot create process");
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
|
||||||
|
bool fLoadedDll = hLibrary != nullptr;
|
||||||
|
if (useConptyDll && fLoadedDll)
|
||||||
|
{
|
||||||
|
PFNRELEASEPSEUDOCONSOLE const pfnReleasePseudoConsole = (PFNRELEASEPSEUDOCONSOLE)GetProcAddress(
|
||||||
|
(HMODULE)hLibrary, "ConptyReleasePseudoConsole");
|
||||||
|
if (pfnReleasePseudoConsole)
|
||||||
|
{
|
||||||
|
pfnReleasePseudoConsole(handle->hpc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update handle
|
||||||
|
handle->hShell = piClient.hProcess;
|
||||||
|
|
||||||
|
// Close the thread handle to avoid resource leak
|
||||||
|
CloseHandle(piClient.hThread);
|
||||||
|
// Close the input read and output write handle of the pseudoconsole
|
||||||
|
CloseHandle(handle->hIn);
|
||||||
|
CloseHandle(handle->hOut);
|
||||||
|
|
||||||
|
SetupExitCallback(env, exitCallback, handle);
|
||||||
|
|
||||||
|
// Return
|
||||||
|
auto marshal = Napi::Object::New(env);
|
||||||
|
marshal.Set("pid", Napi::Number::New(env, piClient.dwProcessId));
|
||||||
|
return marshal;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Napi::Value PtyResize(const Napi::CallbackInfo& info) {
|
||||||
|
Napi::Env env(info.Env());
|
||||||
|
Napi::HandleScope scope(env);
|
||||||
|
|
||||||
|
if (info.Length() != 4 ||
|
||||||
|
!info[0].IsNumber() ||
|
||||||
|
!info[1].IsNumber() ||
|
||||||
|
!info[2].IsNumber() ||
|
||||||
|
!info[3].IsBoolean()) {
|
||||||
|
throw Napi::Error::New(env, "Usage: pty.resize(id, cols, rows, useConptyDll)");
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = info[0].As<Napi::Number>().Int32Value();
|
||||||
|
SHORT cols = static_cast<SHORT>(info[1].As<Napi::Number>().Uint32Value());
|
||||||
|
SHORT rows = static_cast<SHORT>(info[2].As<Napi::Number>().Uint32Value());
|
||||||
|
const bool useConptyDll = info[3].As<Napi::Boolean>().Value();
|
||||||
|
|
||||||
|
const pty_baton* handle = get_pty_baton(id);
|
||||||
|
|
||||||
|
if (handle != nullptr) {
|
||||||
|
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
|
||||||
|
bool fLoadedDll = hLibrary != nullptr;
|
||||||
|
if (fLoadedDll)
|
||||||
|
{
|
||||||
|
PFNRESIZEPSEUDOCONSOLE const pfnResizePseudoConsole = (PFNRESIZEPSEUDOCONSOLE)GetProcAddress(
|
||||||
|
(HMODULE)hLibrary,
|
||||||
|
useConptyDll ? "ConptyResizePseudoConsole" : "ResizePseudoConsole");
|
||||||
|
if (pfnResizePseudoConsole)
|
||||||
|
{
|
||||||
|
COORD size = {cols, rows};
|
||||||
|
pfnResizePseudoConsole(handle->hpc, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return env.Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Napi::Value PtyClear(const Napi::CallbackInfo& info) {
|
||||||
|
Napi::Env env(info.Env());
|
||||||
|
Napi::HandleScope scope(env);
|
||||||
|
|
||||||
|
if (info.Length() != 2 ||
|
||||||
|
!info[0].IsNumber() ||
|
||||||
|
!info[1].IsBoolean()) {
|
||||||
|
throw Napi::Error::New(env, "Usage: pty.clear(id, useConptyDll)");
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = info[0].As<Napi::Number>().Int32Value();
|
||||||
|
const bool useConptyDll = info[1].As<Napi::Boolean>().Value();
|
||||||
|
|
||||||
|
// This API is only supported for conpty.dll as it was introduced in a later version of Windows.
|
||||||
|
// We could hook it up to point at >= a version of Windows only, but the future is conpty.dll
|
||||||
|
// anyway.
|
||||||
|
if (!useConptyDll) {
|
||||||
|
return env.Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
const pty_baton* handle = get_pty_baton(id);
|
||||||
|
|
||||||
|
if (handle != nullptr) {
|
||||||
|
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
|
||||||
|
bool fLoadedDll = hLibrary != nullptr;
|
||||||
|
if (fLoadedDll)
|
||||||
|
{
|
||||||
|
PFNCLEARPSEUDOCONSOLE const pfnClearPseudoConsole = (PFNCLEARPSEUDOCONSOLE)GetProcAddress((HMODULE)hLibrary, "ConptyClearPseudoConsole");
|
||||||
|
if (pfnClearPseudoConsole)
|
||||||
|
{
|
||||||
|
pfnClearPseudoConsole(handle->hpc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return env.Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Napi::Value PtyKill(const Napi::CallbackInfo& info) {
|
||||||
|
Napi::Env env(info.Env());
|
||||||
|
Napi::HandleScope scope(env);
|
||||||
|
|
||||||
|
if (info.Length() != 2 ||
|
||||||
|
!info[0].IsNumber() ||
|
||||||
|
!info[1].IsBoolean()) {
|
||||||
|
throw Napi::Error::New(env, "Usage: pty.kill(id, useConptyDll)");
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = info[0].As<Napi::Number>().Int32Value();
|
||||||
|
const bool useConptyDll = info[1].As<Napi::Boolean>().Value();
|
||||||
|
|
||||||
|
const pty_baton* handle = get_pty_baton(id);
|
||||||
|
|
||||||
|
if (handle != nullptr) {
|
||||||
|
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
|
||||||
|
bool fLoadedDll = hLibrary != nullptr;
|
||||||
|
if (fLoadedDll)
|
||||||
|
{
|
||||||
|
PFNCLOSEPSEUDOCONSOLE const pfnClosePseudoConsole = (PFNCLOSEPSEUDOCONSOLE)GetProcAddress(
|
||||||
|
(HMODULE)hLibrary,
|
||||||
|
useConptyDll ? "ConptyClosePseudoConsole" : "ClosePseudoConsole");
|
||||||
|
if (pfnClosePseudoConsole)
|
||||||
|
{
|
||||||
|
pfnClosePseudoConsole(handle->hpc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (useConptyDll) {
|
||||||
|
TerminateProcess(handle->hShell, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return env.Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init
|
||||||
|
*/
|
||||||
|
|
||||||
|
Napi::Object init(Napi::Env env, Napi::Object exports) {
|
||||||
|
exports.Set("startProcess", Napi::Function::New(env, PtyStartProcess));
|
||||||
|
exports.Set("connect", Napi::Function::New(env, PtyConnect));
|
||||||
|
exports.Set("resize", Napi::Function::New(env, PtyResize));
|
||||||
|
exports.Set("clear", Napi::Function::New(env, PtyClear));
|
||||||
|
exports.Set("kill", Napi::Function::New(env, PtyKill));
|
||||||
|
return exports;
|
||||||
|
};
|
||||||
|
|
||||||
|
NODE_API_MODULE(NODE_GYP_MODULE_NAME, init);
|
||||||
41
web/package/node-pty/src/win/conpty.h
Normal file
41
web/package/node-pty/src/win/conpty.h
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright (c) Microsoft Corporation.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
// This header prototypes the Pseudoconsole symbols from conpty.lib with their original names.
|
||||||
|
// This is required because we cannot import __imp_CreatePseudoConsole from a static library
|
||||||
|
// as it doesn't produce an import lib.
|
||||||
|
// We can't use an /ALTERNATENAME trick because it seems that that name is only resolved when the
|
||||||
|
// linker cannot otherwise find the symbol.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <consoleapi.h>
|
||||||
|
|
||||||
|
#ifndef CONPTY_IMPEXP
|
||||||
|
#define CONPTY_IMPEXP __declspec(dllimport)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CONPTY_EXPORT
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define CONPTY_EXPORT extern "C" CONPTY_IMPEXP
|
||||||
|
#else
|
||||||
|
#define CONPTY_EXPORT extern CONPTY_IMPEXP
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define PSEUDOCONSOLE_RESIZE_QUIRK (2u)
|
||||||
|
#define PSEUDOCONSOLE_PASSTHROUGH_MODE (8u)
|
||||||
|
|
||||||
|
CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsole(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
|
||||||
|
CONPTY_EXPORT HRESULT WINAPI ConptyCreatePseudoConsoleAsUser(HANDLE hToken, COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
|
||||||
|
|
||||||
|
CONPTY_EXPORT HRESULT WINAPI ConptyResizePseudoConsole(HPCON hPC, COORD size);
|
||||||
|
CONPTY_EXPORT HRESULT WINAPI ConptyClearPseudoConsole(HPCON hPC);
|
||||||
|
CONPTY_EXPORT HRESULT WINAPI ConptyShowHidePseudoConsole(HPCON hPC, bool show);
|
||||||
|
CONPTY_EXPORT HRESULT WINAPI ConptyReparentPseudoConsole(HPCON hPC, HWND newParent);
|
||||||
|
CONPTY_EXPORT HRESULT WINAPI ConptyReleasePseudoConsole(HPCON hPC);
|
||||||
|
|
||||||
|
CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsole(HPCON hPC);
|
||||||
|
CONPTY_EXPORT VOID WINAPI ConptyClosePseudoConsoleTimeout(HPCON hPC, DWORD dwMilliseconds);
|
||||||
|
|
||||||
|
CONPTY_EXPORT HRESULT WINAPI ConptyPackPseudoConsole(HANDLE hServerProcess, HANDLE hRef, HANDLE hSignal, HPCON* phPC);
|
||||||
44
web/package/node-pty/src/win/conpty_console_list.cc
Normal file
44
web/package/node-pty/src/win/conpty_console_list.cc
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2019, Microsoft Corporation (MIT License).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define NODE_ADDON_API_DISABLE_DEPRECATED
|
||||||
|
#include <napi.h>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
static Napi::Value ApiConsoleProcessList(const Napi::CallbackInfo& info) {
|
||||||
|
Napi::Env env(info.Env());
|
||||||
|
if (info.Length() != 1 ||
|
||||||
|
!info[0].IsNumber()) {
|
||||||
|
throw Napi::Error::New(env, "Usage: getConsoleProcessList(shellPid)");
|
||||||
|
}
|
||||||
|
|
||||||
|
const DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
||||||
|
|
||||||
|
if (!FreeConsole()) {
|
||||||
|
throw Napi::Error::New(env, "FreeConsole failed");
|
||||||
|
}
|
||||||
|
if (!AttachConsole(pid)) {
|
||||||
|
throw Napi::Error::New(env, "AttachConsole failed");
|
||||||
|
}
|
||||||
|
auto processList = std::vector<DWORD>(64);
|
||||||
|
auto processCount = GetConsoleProcessList(&processList[0], static_cast<DWORD>(processList.size()));
|
||||||
|
if (processList.size() < processCount) {
|
||||||
|
processList.resize(processCount);
|
||||||
|
processCount = GetConsoleProcessList(&processList[0], static_cast<DWORD>(processList.size()));
|
||||||
|
}
|
||||||
|
FreeConsole();
|
||||||
|
|
||||||
|
Napi::Array result = Napi::Array::New(env);
|
||||||
|
for (DWORD i = 0; i < processCount; i++) {
|
||||||
|
result.Set(i, Napi::Number::New(env, processList[i]));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Napi::Object init(Napi::Env env, Napi::Object exports) {
|
||||||
|
exports.Set("getConsoleProcessList", Napi::Function::New(env, ApiConsoleProcessList));
|
||||||
|
return exports;
|
||||||
|
};
|
||||||
|
|
||||||
|
NODE_API_MODULE(NODE_GYP_MODULE_NAME, init);
|
||||||
95
web/package/node-pty/src/win/path_util.cc
Normal file
95
web/package/node-pty/src/win/path_util.cc
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
/**
|
||||||
|
* 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 <stdexcept>
|
||||||
|
#include <Shlwapi.h> // PathCombine
|
||||||
|
#include <Windows.h>
|
||||||
|
#include "path_util.h"
|
||||||
|
|
||||||
|
namespace path_util {
|
||||||
|
|
||||||
|
std::wstring to_wstring(const Napi::String& str) {
|
||||||
|
const std::u16string & u16 = str.Utf16Value();
|
||||||
|
return std::wstring(u16.begin(), u16.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string wstring_to_string(const std::wstring &wide_string) {
|
||||||
|
if (wide_string.empty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const auto size_needed = WideCharToMultiByte(CP_UTF8, 0, &wide_string.at(0), (int)wide_string.size(), nullptr, 0, nullptr, nullptr);
|
||||||
|
if (size_needed <= 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
std::string result(size_needed, 0);
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, &wide_string.at(0), (int)wide_string.size(), &result.at(0), size_needed, nullptr, nullptr);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string from_wstring(const wchar_t* wstr) {
|
||||||
|
int bufferSize = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL);
|
||||||
|
if (bufferSize <= 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
std::string result(bufferSize - 1, 0); // -1 to exclude null terminator
|
||||||
|
int status = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &result[0], bufferSize, NULL, NULL);
|
||||||
|
if (status == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_ = new wchar_t[MAX_ENV];
|
||||||
|
int read = ::GetEnvironmentVariableW(L"Path", buffer_, MAX_ENV);
|
||||||
|
if (read) {
|
||||||
|
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 (size_t 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete[] buffer_;
|
||||||
|
return shellpath;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace path_util
|
||||||
26
web/package/node-pty/src/win/path_util.h
Normal file
26
web/package/node-pty/src/win/path_util.h
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* 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_
|
||||||
|
|
||||||
|
#define NODE_ADDON_API_DISABLE_DEPRECATED
|
||||||
|
#include <napi.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#define MAX_ENV 65536
|
||||||
|
|
||||||
|
namespace path_util {
|
||||||
|
|
||||||
|
std::wstring to_wstring(const Napi::String& str);
|
||||||
|
std::string wstring_to_string(const std::wstring &wide_string);
|
||||||
|
std::string from_wstring(const wchar_t* wstr);
|
||||||
|
bool file_exists(std::wstring filename);
|
||||||
|
std::wstring get_shell_path(std::wstring filename);
|
||||||
|
|
||||||
|
} // namespace path_util
|
||||||
|
|
||||||
|
#endif // NODE_PTY_PATH_UTIL_H_
|
||||||
333
web/package/node-pty/src/win/winpty.cc
Normal file
333
web/package/node-pty/src/win/winpty.cc
Normal file
|
|
@ -0,0 +1,333 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define NODE_ADDON_API_DISABLE_DEPRECATED
|
||||||
|
#include <napi.h>
|
||||||
|
#include <iostream>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <map>
|
||||||
|
#include <Shlwapi.h> // PathCombine, PathIsRelative
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <winpty.h>
|
||||||
|
|
||||||
|
#include "path_util.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Misc
|
||||||
|
*/
|
||||||
|
#define WINPTY_DBG_VARIABLE TEXT("WINPTYDBG")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* winpty
|
||||||
|
*/
|
||||||
|
static std::vector<winpty_t *> ptyHandles;
|
||||||
|
static volatile LONG ptyCounter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Keeps track of the handles created by PtyStartProcess */
|
||||||
|
static std::map<DWORD, HANDLE> createdHandles;
|
||||||
|
|
||||||
|
static winpty_t *get_pipe_handle(DWORD pid) {
|
||||||
|
for (size_t i = 0; i < ptyHandles.size(); ++i) {
|
||||||
|
winpty_t *ptyHandle = ptyHandles[i];
|
||||||
|
HANDLE current = winpty_agent_process(ptyHandle);
|
||||||
|
if (GetProcessId(current) == pid) {
|
||||||
|
return ptyHandle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool remove_pipe_handle(DWORD pid) {
|
||||||
|
for (size_t i = 0; i < ptyHandles.size(); ++i) {
|
||||||
|
winpty_t *ptyHandle = ptyHandles[i];
|
||||||
|
HANDLE current = winpty_agent_process(ptyHandle);
|
||||||
|
if (GetProcessId(current) == pid) {
|
||||||
|
winpty_free(ptyHandle);
|
||||||
|
ptyHandles.erase(ptyHandles.begin() + i);
|
||||||
|
ptyHandle = nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Napi::Error error_with_winpty_msg(const char *generalMsg, winpty_error_ptr_t error_ptr, Napi::Env env) {
|
||||||
|
std::string why;
|
||||||
|
why += generalMsg;
|
||||||
|
why += ": ";
|
||||||
|
why += path_util::wstring_to_string(winpty_error_msg(error_ptr));
|
||||||
|
winpty_error_free(error_ptr);
|
||||||
|
return Napi::Error::New(env, why);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Napi::Value PtyGetExitCode(const Napi::CallbackInfo& info) {
|
||||||
|
Napi::Env env(info.Env());
|
||||||
|
Napi::HandleScope scope(env);
|
||||||
|
|
||||||
|
if (info.Length() != 1 ||
|
||||||
|
!info[0].IsNumber()) {
|
||||||
|
throw Napi::Error::New(env, "Usage: pty.getExitCode(pid)");
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
||||||
|
HANDLE handle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
|
||||||
|
if (handle == NULL) {
|
||||||
|
return Napi::Number::New(env, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD exitCode = 0;
|
||||||
|
BOOL success = GetExitCodeProcess(handle, &exitCode);
|
||||||
|
if (success == FALSE) {
|
||||||
|
exitCode = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(handle);
|
||||||
|
return Napi::Number::New(env, exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Napi::Value PtyGetProcessList(const Napi::CallbackInfo& info) {
|
||||||
|
Napi::Env env(info.Env());
|
||||||
|
Napi::HandleScope scope(env);
|
||||||
|
|
||||||
|
if (info.Length() != 1 ||
|
||||||
|
!info[0].IsNumber()) {
|
||||||
|
throw Napi::Error::New(env, "Usage: pty.getProcessList(pid)");
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
||||||
|
winpty_t *pc = get_pipe_handle(pid);
|
||||||
|
if (pc == nullptr) {
|
||||||
|
return Napi::Number::New(env, 0);
|
||||||
|
}
|
||||||
|
int processList[64];
|
||||||
|
const int processCount = 64;
|
||||||
|
int actualCount = winpty_get_console_process_list(pc, processList, processCount, nullptr);
|
||||||
|
if (actualCount <= 0) {
|
||||||
|
return Napi::Number::New(env, 0);
|
||||||
|
}
|
||||||
|
Napi::Array result = Napi::Array::New(env, actualCount);
|
||||||
|
for (int i = 0; i < actualCount; i++) {
|
||||||
|
result.Set(i, Napi::Number::New(env, processList[i]));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Napi::Value PtyStartProcess(const Napi::CallbackInfo& info) {
|
||||||
|
Napi::Env env(info.Env());
|
||||||
|
Napi::HandleScope scope(env);
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
throw Napi::Error::New(env, "Usage: pty.startProcess(file, cmdline, env, cwd, cols, rows, debug)");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring filename(path_util::to_wstring(info[0].As<Napi::String>()));
|
||||||
|
std::wstring cmdline(path_util::to_wstring(info[1].As<Napi::String>()));
|
||||||
|
std::wstring cwd(path_util::to_wstring(info[3].As<Napi::String>()));
|
||||||
|
|
||||||
|
// create environment block
|
||||||
|
std::wstring envStr;
|
||||||
|
const Napi::Array envValues = info[2].As<Napi::Array>();
|
||||||
|
if (!envValues.IsEmpty()) {
|
||||||
|
std::wstring envBlock;
|
||||||
|
for(uint32_t i = 0; i < envValues.Length(); i++) {
|
||||||
|
envBlock += path_util::to_wstring(envValues.Get(i).As<Napi::String>());
|
||||||
|
envBlock += L'\0';
|
||||||
|
}
|
||||||
|
envStr = std::move(envBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// use environment 'Path' variable to determine location of
|
||||||
|
// the relative path that we have recieved (e.g cmd.exe)
|
||||||
|
std::wstring shellpath;
|
||||||
|
if (::PathIsRelativeW(filename.c_str())) {
|
||||||
|
shellpath = path_util::get_shell_path(filename);
|
||||||
|
} else {
|
||||||
|
shellpath = filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shellpath.empty() || !path_util::file_exists(shellpath)) {
|
||||||
|
std::string why;
|
||||||
|
why += "File not found: ";
|
||||||
|
why += path_util::wstring_to_string(shellpath);
|
||||||
|
throw Napi::Error::New(env, why);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cols = info[4].As<Napi::Number>().Int32Value();
|
||||||
|
int rows = info[5].As<Napi::Number>().Int32Value();
|
||||||
|
bool debug = info[6].As<Napi::Boolean>().Value();
|
||||||
|
|
||||||
|
// 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 error_with_winpty_msg("Error creating WinPTY config", error_ptr, env);
|
||||||
|
}
|
||||||
|
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 error_with_winpty_msg("Error launching WinPTY agent", error_ptr, env);
|
||||||
|
}
|
||||||
|
winpty_error_free(error_ptr);
|
||||||
|
|
||||||
|
// Create winpty spawn config
|
||||||
|
winpty_spawn_config_t* config = winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, shellpath.c_str(), cmdline.c_str(), cwd.c_str(), envStr.c_str(), &error_ptr);
|
||||||
|
if (config == nullptr) {
|
||||||
|
winpty_free(pc);
|
||||||
|
throw error_with_winpty_msg("Error creating WinPTY spawn config", error_ptr, env);
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
if (handle) {
|
||||||
|
CloseHandle(handle);
|
||||||
|
}
|
||||||
|
winpty_free(pc);
|
||||||
|
throw error_with_winpty_msg("Unable to start terminal process", error_ptr, env);
|
||||||
|
}
|
||||||
|
winpty_error_free(error_ptr);
|
||||||
|
|
||||||
|
LPCWSTR coninPipeName = winpty_conin_name(pc);
|
||||||
|
std::string coninPipeNameStr = path_util::from_wstring(coninPipeName);
|
||||||
|
if (coninPipeNameStr.empty()) {
|
||||||
|
CloseHandle(handle);
|
||||||
|
winpty_free(pc);
|
||||||
|
throw Napi::Error::New(env, "Failed to initialize winpty conin");
|
||||||
|
}
|
||||||
|
|
||||||
|
LPCWSTR conoutPipeName = winpty_conout_name(pc);
|
||||||
|
std::string conoutPipeNameStr = path_util::from_wstring(conoutPipeName);
|
||||||
|
if (conoutPipeNameStr.empty()) {
|
||||||
|
CloseHandle(handle);
|
||||||
|
winpty_free(pc);
|
||||||
|
throw Napi::Error::New(env, "Failed to initialize winpty conout");
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD innerPid = GetProcessId(handle);
|
||||||
|
if (createdHandles[innerPid]) {
|
||||||
|
CloseHandle(handle);
|
||||||
|
winpty_free(pc);
|
||||||
|
std::stringstream why;
|
||||||
|
why << "There is already a process with innerPid " << innerPid;
|
||||||
|
throw Napi::Error::New(env, why.str());
|
||||||
|
}
|
||||||
|
createdHandles[innerPid] = handle;
|
||||||
|
|
||||||
|
// Save pty struct for later use
|
||||||
|
ptyHandles.push_back(pc);
|
||||||
|
|
||||||
|
DWORD pid = GetProcessId(winpty_agent_process(pc));
|
||||||
|
Napi::Object marshal = Napi::Object::New(env);
|
||||||
|
marshal.Set("innerPid", Napi::Number::New(env, (int)innerPid));
|
||||||
|
marshal.Set("pid", Napi::Number::New(env, (int)pid));
|
||||||
|
marshal.Set("pty", Napi::Number::New(env, InterlockedIncrement(&ptyCounter)));
|
||||||
|
marshal.Set("fd", Napi::Number::New(env, -1));
|
||||||
|
marshal.Set("conin", Napi::String::New(env, coninPipeNameStr));
|
||||||
|
marshal.Set("conout", Napi::String::New(env, conoutPipeNameStr));
|
||||||
|
|
||||||
|
return marshal;
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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(pid, cols, rows)");
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
||||||
|
int cols = info[1].As<Napi::Number>().Int32Value();
|
||||||
|
int rows = info[2].As<Napi::Number>().Int32Value();
|
||||||
|
|
||||||
|
winpty_t *pc = get_pipe_handle(pid);
|
||||||
|
|
||||||
|
if (pc == nullptr) {
|
||||||
|
throw Napi::Error::New(env, "The pty doesn't appear to exist");
|
||||||
|
}
|
||||||
|
BOOL success = winpty_set_size(pc, cols, rows, nullptr);
|
||||||
|
if (!success) {
|
||||||
|
throw Napi::Error::New(env, "The pty could not be resized");
|
||||||
|
}
|
||||||
|
|
||||||
|
return env.Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Napi::Value PtyKill(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.kill(pid, innerPid)");
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD pid = info[0].As<Napi::Number>().Uint32Value();
|
||||||
|
DWORD innerPid = info[1].As<Napi::Number>().Uint32Value();
|
||||||
|
|
||||||
|
winpty_t *pc = get_pipe_handle(pid);
|
||||||
|
if (pc == nullptr) {
|
||||||
|
throw Napi::Error::New(env, "Pty seems to have been killed already");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(remove_pipe_handle(pid));
|
||||||
|
|
||||||
|
HANDLE innerPidHandle = createdHandles[innerPid];
|
||||||
|
createdHandles.erase(innerPid);
|
||||||
|
CloseHandle(innerPidHandle);
|
||||||
|
|
||||||
|
return env.Undefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init
|
||||||
|
*/
|
||||||
|
|
||||||
|
Napi::Object init(Napi::Env env, Napi::Object exports) {
|
||||||
|
exports.Set("startProcess", Napi::Function::New(env, PtyStartProcess));
|
||||||
|
exports.Set("resize", Napi::Function::New(env, PtyResize));
|
||||||
|
exports.Set("kill", Napi::Function::New(env, PtyKill));
|
||||||
|
exports.Set("getExitCode", Napi::Function::New(env, PtyGetExitCode));
|
||||||
|
exports.Set("getProcessList", Napi::Function::New(env, PtyGetProcessList));
|
||||||
|
return exports;
|
||||||
|
};
|
||||||
|
|
||||||
|
NODE_API_MODULE(NODE_GYP_MODULE_NAME, init);
|
||||||
214
web/package/node-pty/src/windowsTerminal.ts
Normal file
214
web/package/node-pty/src/windowsTerminal.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
/**
|
||||||
|
* Simplified Windows terminal implementation without threading
|
||||||
|
* Removed ConoutSocketWorker and shared pipe architecture
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { Socket } from 'net';
|
||||||
|
import { Terminal, DEFAULT_COLS, DEFAULT_ROWS } from './terminal';
|
||||||
|
import { IPtyOpenOptions, IWindowsPtyForkOptions } from './interfaces';
|
||||||
|
import { ArgvOrCommandLine } from './types';
|
||||||
|
import { assign } from './utils';
|
||||||
|
|
||||||
|
const DEFAULT_FILE = 'cmd.exe';
|
||||||
|
const DEFAULT_NAME = 'Windows Shell';
|
||||||
|
|
||||||
|
// Native module interfaces
|
||||||
|
interface IConptyProcess {
|
||||||
|
pty: number;
|
||||||
|
fd: number;
|
||||||
|
conin: string;
|
||||||
|
conout: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IConptyNative {
|
||||||
|
startProcess(file: string, cols: number, rows: number, debug: boolean, pipeName: string, inheritCursor: boolean, useConptyDll: boolean): IConptyProcess;
|
||||||
|
connect(pty: number, commandLine: string, cwd: string, env: string[], useConptyDll: boolean, onExit: (exitCode: number) => void): { pid: number };
|
||||||
|
resize(pty: number, cols: number, rows: number, useConptyDll: boolean): void;
|
||||||
|
clear(pty: number, useConptyDll: boolean): void;
|
||||||
|
kill(pty: number, useConptyDll: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let conptyNative: IConptyNative;
|
||||||
|
|
||||||
|
export class WindowsTerminal extends Terminal {
|
||||||
|
private _isReady: boolean = false;
|
||||||
|
protected _pid: number = 0;
|
||||||
|
private _innerPid: number = 0;
|
||||||
|
private _ptyNative: IConptyNative;
|
||||||
|
protected _pty: number;
|
||||||
|
private _inSocket!: Socket;
|
||||||
|
private _outSocket!: Socket;
|
||||||
|
private _exitCode: number | undefined;
|
||||||
|
private _useConptyDll: boolean = false;
|
||||||
|
|
||||||
|
public get master(): Socket | undefined { return this._outSocket; }
|
||||||
|
public get slave(): Socket | undefined { return this._inSocket; }
|
||||||
|
|
||||||
|
constructor(file?: string, args?: ArgvOrCommandLine, opt?: IWindowsPtyForkOptions) {
|
||||||
|
super(opt);
|
||||||
|
|
||||||
|
// Load native module
|
||||||
|
if (!conptyNative) {
|
||||||
|
try {
|
||||||
|
conptyNative = require('../build/Release/conpty.node');
|
||||||
|
} catch (outerError) {
|
||||||
|
try {
|
||||||
|
conptyNative = require('../build/Debug/conpty.node');
|
||||||
|
} catch (innerError) {
|
||||||
|
throw outerError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._ptyNative = conptyNative;
|
||||||
|
|
||||||
|
// Initialize arguments
|
||||||
|
args = args || [];
|
||||||
|
file = file || DEFAULT_FILE;
|
||||||
|
opt = opt || {};
|
||||||
|
opt.env = opt.env || process.env;
|
||||||
|
|
||||||
|
const env = assign({}, opt.env);
|
||||||
|
this._cols = opt.cols || DEFAULT_COLS;
|
||||||
|
this._rows = opt.rows || DEFAULT_ROWS;
|
||||||
|
const cwd = opt.cwd || process.cwd();
|
||||||
|
const parsedEnv = this._parseEnv(env);
|
||||||
|
|
||||||
|
// Compose command line
|
||||||
|
const commandLine = this._argsToCommandLine(file, args);
|
||||||
|
|
||||||
|
// Start ConPTY process
|
||||||
|
const pipeName = this._generatePipeName();
|
||||||
|
const term = this._ptyNative.startProcess(file, this._cols, this._rows, false, pipeName, false, this._useConptyDll);
|
||||||
|
|
||||||
|
this._pty = term.pty;
|
||||||
|
this._fd = term.fd;
|
||||||
|
|
||||||
|
// Create direct socket connections without worker threads
|
||||||
|
this._setupDirectSockets(term);
|
||||||
|
|
||||||
|
// Connect the process
|
||||||
|
const connect = this._ptyNative.connect(this._pty, commandLine, cwd, parsedEnv, this._useConptyDll, (exitCode) => {
|
||||||
|
this._exitCode = exitCode;
|
||||||
|
this.emit('exit', exitCode);
|
||||||
|
this._close();
|
||||||
|
});
|
||||||
|
this._innerPid = connect.pid;
|
||||||
|
this._pid = connect.pid;
|
||||||
|
|
||||||
|
this._file = file;
|
||||||
|
this._name = opt.name || env.TERM || DEFAULT_NAME;
|
||||||
|
this._readable = true;
|
||||||
|
this._writable = true;
|
||||||
|
|
||||||
|
this._forwardEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setupDirectSockets(term: IConptyProcess): void {
|
||||||
|
// Setup output socket - read directly from conout
|
||||||
|
const outFd = fs.openSync(term.conout, 'r');
|
||||||
|
this._outSocket = new Socket({ fd: outFd, readable: true, writable: false });
|
||||||
|
this._outSocket.setEncoding('utf8');
|
||||||
|
this._socket = this._outSocket;
|
||||||
|
|
||||||
|
// Setup input socket - write directly to conin
|
||||||
|
const inFd = fs.openSync(term.conin, 'w');
|
||||||
|
this._inSocket = new Socket({ fd: inFd, readable: false, writable: true });
|
||||||
|
this._inSocket.setEncoding('utf8');
|
||||||
|
|
||||||
|
// Forward events directly
|
||||||
|
this._outSocket.on('data', (data) => {
|
||||||
|
if (!this._isReady) {
|
||||||
|
this._isReady = true;
|
||||||
|
}
|
||||||
|
this.emit('data', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._outSocket.on('error', (err) => {
|
||||||
|
if ((err as any).code && ((err as any).code.includes('EPIPE') || (err as any).code.includes('EIO'))) {
|
||||||
|
// Expected errors when process exits
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.emit('error', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._outSocket.on('close', () => {
|
||||||
|
if (this._exitCode === undefined) {
|
||||||
|
this.emit('exit', 0);
|
||||||
|
}
|
||||||
|
this._close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _write(data: string): void {
|
||||||
|
if (this._inSocket && this._inSocket.writable) {
|
||||||
|
this._inSocket.write(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public resize(cols: number, rows: number): void {
|
||||||
|
if (cols <= 0 || rows <= 0 || isNaN(cols) || isNaN(rows) || cols === Infinity || rows === Infinity) {
|
||||||
|
throw new Error('resizing must be done using positive cols and rows');
|
||||||
|
}
|
||||||
|
if (this._exitCode !== undefined) {
|
||||||
|
throw new Error('Cannot resize a pty that has already exited');
|
||||||
|
}
|
||||||
|
this._cols = cols;
|
||||||
|
this._rows = rows;
|
||||||
|
this._ptyNative.resize(this._pty, cols, rows, this._useConptyDll);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this._ptyNative.clear(this._pty, this._useConptyDll);
|
||||||
|
}
|
||||||
|
|
||||||
|
public kill(signal?: string): void {
|
||||||
|
this._close();
|
||||||
|
try {
|
||||||
|
process.kill(this._pid);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore if process cannot be found
|
||||||
|
}
|
||||||
|
this._ptyNative.kill(this._pty, this._useConptyDll);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _close(): void {
|
||||||
|
if (this._inSocket) {
|
||||||
|
this._inSocket.destroy();
|
||||||
|
}
|
||||||
|
if (this._outSocket) {
|
||||||
|
this._outSocket.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generatePipeName(): string {
|
||||||
|
return `\\\\.\\pipe\\conpty-${Date.now()}-${Math.random()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _argsToCommandLine(file: string, args: ArgvOrCommandLine): string {
|
||||||
|
if (typeof args === 'string') {
|
||||||
|
return `${file} ${args}`;
|
||||||
|
}
|
||||||
|
const argv = [file];
|
||||||
|
if (args) {
|
||||||
|
argv.push(...args);
|
||||||
|
}
|
||||||
|
return argv.map(arg => {
|
||||||
|
if (arg.includes(' ') || arg.includes('\t')) {
|
||||||
|
return `"${arg.replace(/"/g, '\\"')}"`;
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static open(options?: IPtyOpenOptions): void {
|
||||||
|
throw new Error('open() not supported on windows, use spawn() instead.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public get process(): string { return this._name; }
|
||||||
|
public get pid(): number { return this._pid; }
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
78
web/package/package.json
Normal file
78
web/package/package.json
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
{
|
||||||
|
"name": "vibetunnel",
|
||||||
|
"version": "1.0.0-beta.12",
|
||||||
|
"description": "Terminal sharing server with web interface - supports macOS, Linux, and headless environments",
|
||||||
|
"main": "lib/cli.js",
|
||||||
|
"bin": {
|
||||||
|
"vibetunnel": "./bin/vibetunnel",
|
||||||
|
"vt": "./bin/vt"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib/",
|
||||||
|
"public/",
|
||||||
|
"bin/",
|
||||||
|
"scripts/",
|
||||||
|
"node-pty/",
|
||||||
|
"node_modules/authenticate-pam/",
|
||||||
|
"prebuilds/",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"os": [
|
||||||
|
"darwin",
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/amantus-ai/vibetunnel.git",
|
||||||
|
"directory": "web"
|
||||||
|
},
|
||||||
|
"homepage": "https://vibetunnel.sh",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/amantus-ai/vibetunnel/issues"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "node scripts/postinstall.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/commands": "^6.8.1",
|
||||||
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/lang-markdown": "^6.3.3",
|
||||||
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/state": "^6.5.2",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"@codemirror/view": "^6.38.0",
|
||||||
|
"@xterm/headless": "^5.5.0",
|
||||||
|
"bonjour-service": "^1.3.0",
|
||||||
|
"chalk": "^5.4.1",
|
||||||
|
"compression": "^1.8.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
|
"http-proxy-middleware": "^3.0.5",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"lit": "^3.3.1",
|
||||||
|
"mime-types": "^3.0.1",
|
||||||
|
"monaco-editor": "^0.52.2",
|
||||||
|
"multer": "^2.0.1",
|
||||||
|
"postject": "1.0.0-alpha.6",
|
||||||
|
"signal-exit": "^4.1.0",
|
||||||
|
"web-push": "^3.6.7",
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"authenticate-pam": "^1.0.5"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"terminal",
|
||||||
|
"multiplexer",
|
||||||
|
"websocket",
|
||||||
|
"asciinema"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
73
web/package/scripts/node-pty-plugin.js
Normal file
73
web/package/scripts/node-pty-plugin.js
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* ESBuild plugin to handle node-pty resolution for npm packages
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const nodePtyPlugin = {
|
||||||
|
name: 'node-pty-resolver',
|
||||||
|
setup(build) {
|
||||||
|
// Resolve node-pty imports to our bundled version
|
||||||
|
build.onResolve({ filter: /^node-pty$/ }, args => {
|
||||||
|
// In development, use the normal node_modules resolution
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For npm builds, resolve to our bundled node-pty
|
||||||
|
return {
|
||||||
|
path: 'node-pty',
|
||||||
|
namespace: 'node-pty-stub'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Provide stub that dynamically loads the bundled node-pty
|
||||||
|
build.onLoad({ filter: /^node-pty$/, namespace: 'node-pty-stub' }, () => {
|
||||||
|
return {
|
||||||
|
contents: `
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Try multiple possible locations for node-pty
|
||||||
|
const possiblePaths = [
|
||||||
|
// When installed via npm
|
||||||
|
path.join(__dirname, '../node-pty'),
|
||||||
|
path.join(__dirname, '../../node-pty'),
|
||||||
|
// During development
|
||||||
|
path.join(__dirname, '../node_modules/node-pty'),
|
||||||
|
// Fallback to regular require
|
||||||
|
'node-pty'
|
||||||
|
];
|
||||||
|
|
||||||
|
let nodePty;
|
||||||
|
let loadError;
|
||||||
|
|
||||||
|
for (const tryPath of possiblePaths) {
|
||||||
|
try {
|
||||||
|
if (tryPath === 'node-pty') {
|
||||||
|
// Try regular require as last resort
|
||||||
|
nodePty = require(tryPath);
|
||||||
|
} else if (fs.existsSync(tryPath)) {
|
||||||
|
// Check if the path exists before trying to load
|
||||||
|
nodePty = require(tryPath);
|
||||||
|
}
|
||||||
|
if (nodePty) break;
|
||||||
|
} catch (err) {
|
||||||
|
loadError = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodePty) {
|
||||||
|
throw new Error(\`Failed to load node-pty from any location. Last error: \${loadError?.message}\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nodePty;
|
||||||
|
`,
|
||||||
|
loader: 'js'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { nodePtyPlugin };
|
||||||
257
web/package/scripts/postinstall.js
Executable file
257
web/package/scripts/postinstall.js
Executable file
|
|
@ -0,0 +1,257 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Postinstall script for npm package
|
||||||
|
* Handles prebuild extraction and fallback compilation
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
console.log('Setting up native modules for VibeTunnel...');
|
||||||
|
|
||||||
|
// Check for npm_config_prefix conflict with NVM
|
||||||
|
if (process.env.npm_config_prefix && process.env.NVM_DIR) {
|
||||||
|
const nvmNodeVersion = process.execPath;
|
||||||
|
const npmPrefix = process.env.npm_config_prefix;
|
||||||
|
|
||||||
|
// Check if npm_config_prefix conflicts with NVM path
|
||||||
|
if (!nvmNodeVersion.includes(npmPrefix) && nvmNodeVersion.includes('.nvm')) {
|
||||||
|
console.warn('⚠️ Detected npm_config_prefix conflict with NVM');
|
||||||
|
console.warn(` npm_config_prefix: ${npmPrefix}`);
|
||||||
|
console.warn(` NVM Node path: ${nvmNodeVersion}`);
|
||||||
|
console.warn(' This may cause npm global installs to fail or install in wrong location.');
|
||||||
|
console.warn(' Run: unset npm_config_prefix');
|
||||||
|
console.warn(' Then reinstall VibeTunnel for proper NVM compatibility.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're in development (has src directory) or npm install
|
||||||
|
const isDevelopment = fs.existsSync(path.join(__dirname, '..', 'src'));
|
||||||
|
|
||||||
|
if (isDevelopment) {
|
||||||
|
// In development, run the existing ensure-native-modules script
|
||||||
|
require('./ensure-native-modules.js');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For npm package, node-pty is bundled in the package root
|
||||||
|
// No need to create symlinks as it's accessed directly
|
||||||
|
|
||||||
|
// Get Node ABI version
|
||||||
|
const nodeABI = process.versions.modules;
|
||||||
|
|
||||||
|
// Get platform and architecture
|
||||||
|
const platform = process.platform;
|
||||||
|
const arch = os.arch();
|
||||||
|
|
||||||
|
// Convert architecture names
|
||||||
|
const archMap = {
|
||||||
|
'arm64': 'arm64',
|
||||||
|
'aarch64': 'arm64',
|
||||||
|
'x64': 'x64',
|
||||||
|
'x86_64': 'x64'
|
||||||
|
};
|
||||||
|
const normalizedArch = archMap[arch] || arch;
|
||||||
|
|
||||||
|
console.log(`Platform: ${platform}-${normalizedArch}, Node ABI: ${nodeABI}`);
|
||||||
|
|
||||||
|
// Function to try prebuild-install first
|
||||||
|
const tryPrebuildInstall = (moduleName, moduleDir) => {
|
||||||
|
try {
|
||||||
|
// Check if prebuild-install is available
|
||||||
|
const prebuildInstallPath = require.resolve('prebuild-install/bin.js');
|
||||||
|
console.log(` Attempting to use prebuild-install for ${moduleName}...`);
|
||||||
|
|
||||||
|
execSync(`node "${prebuildInstallPath}"`, {
|
||||||
|
cwd: moduleDir,
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: { ...process.env, npm_config_build_from_source: 'false' }
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` prebuild-install failed for ${moduleName}, will try manual extraction`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to manually extract prebuild
|
||||||
|
const extractPrebuild = (name, version, targetDir, skipDirCheck = false) => {
|
||||||
|
const prebuildFile = path.join(__dirname, '..', 'prebuilds',
|
||||||
|
`${name}-v${version}-node-v${nodeABI}-${platform}-${normalizedArch}.tar.gz`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(prebuildFile)) {
|
||||||
|
console.log(` No prebuild found for ${name} on this platform`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For optional dependencies like authenticate-pam, check if the module exists
|
||||||
|
// If not, extract to a different location
|
||||||
|
let extractDir = targetDir;
|
||||||
|
if (skipDirCheck && name === 'authenticate-pam' && !fs.existsSync(targetDir)) {
|
||||||
|
// Extract to a controlled location since node_modules/authenticate-pam doesn't exist
|
||||||
|
extractDir = path.join(__dirname, '..', 'optional-modules', name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the parent directory
|
||||||
|
fs.mkdirSync(extractDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract directly into the module directory - the tar already contains build/Release structure
|
||||||
|
execSync(`tar -xzf "${prebuildFile}" -C "${extractDir}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`✓ ${name} prebuilt binary extracted`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` Failed to extract ${name} prebuild:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to compile from source
|
||||||
|
const compileFromSource = (moduleName, moduleDir) => {
|
||||||
|
console.log(` Building ${moduleName} from source...`);
|
||||||
|
try {
|
||||||
|
// First check if node-gyp is available
|
||||||
|
try {
|
||||||
|
execSync('node-gyp --version', { stdio: 'pipe' });
|
||||||
|
} catch (e) {
|
||||||
|
console.log(' Installing node-gyp...');
|
||||||
|
execSync('npm install -g node-gyp', { stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// For node-pty, node-addon-api is included as a dependency in its package.json
|
||||||
|
// npm should handle it automatically during source compilation
|
||||||
|
|
||||||
|
execSync('node-gyp rebuild', {
|
||||||
|
cwd: moduleDir,
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
console.log(`✓ ${moduleName} built successfully`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` Failed to build ${moduleName}:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle both native modules
|
||||||
|
const modules = [
|
||||||
|
{
|
||||||
|
name: 'node-pty',
|
||||||
|
version: '1.0.0',
|
||||||
|
dir: path.join(__dirname, '..', 'node-pty'),
|
||||||
|
build: path.join(__dirname, '..', 'node-pty', 'build', 'Release', 'pty.node'),
|
||||||
|
essential: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'authenticate-pam',
|
||||||
|
version: '1.0.5',
|
||||||
|
dir: path.join(__dirname, '..', 'node_modules', 'authenticate-pam'),
|
||||||
|
build: path.join(__dirname, '..', 'node_modules', 'authenticate-pam', 'build', 'Release', 'authenticate_pam.node'),
|
||||||
|
essential: false, // Optional - falls back to other auth methods
|
||||||
|
platforms: ['linux', 'darwin'], // Needed on Linux and macOS
|
||||||
|
skipDirCheck: true // Don't check if dir exists since it's optional
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
for (const module of modules) {
|
||||||
|
console.log(`\nProcessing ${module.name}...`);
|
||||||
|
|
||||||
|
// Skip platform-specific modules if not on that platform
|
||||||
|
if (module.platforms && !module.platforms.includes(platform)) {
|
||||||
|
console.log(` Skipping ${module.name} (not needed on ${platform})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if module directory exists
|
||||||
|
if (!fs.existsSync(module.dir)) {
|
||||||
|
if (module.skipDirCheck) {
|
||||||
|
// For optional modules, we'll try to extract the prebuild anyway
|
||||||
|
console.log(` ${module.name} not installed via npm (optional dependency), will extract prebuild`);
|
||||||
|
} else {
|
||||||
|
console.warn(` Warning: ${module.name} directory not found at ${module.dir}`);
|
||||||
|
if (module.essential) {
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already built
|
||||||
|
if (fs.existsSync(module.build)) {
|
||||||
|
console.log(`✓ ${module.name} already available`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try installation methods in order
|
||||||
|
let success = false;
|
||||||
|
|
||||||
|
// Method 1: Try prebuild-install (preferred) - skip if directory doesn't exist
|
||||||
|
if (fs.existsSync(module.dir)) {
|
||||||
|
success = tryPrebuildInstall(module.name, module.dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Manual prebuild extraction
|
||||||
|
if (!success) {
|
||||||
|
success = extractPrebuild(module.name, module.version, module.dir, module.skipDirCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: Compile from source (skip if directory doesn't exist)
|
||||||
|
if (!success && fs.existsSync(module.dir) && fs.existsSync(path.join(module.dir, 'binding.gyp'))) {
|
||||||
|
success = compileFromSource(module.name, module.dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check final result
|
||||||
|
if (!success) {
|
||||||
|
// Special handling for authenticate-pam on macOS
|
||||||
|
if (module.name === 'authenticate-pam' && process.platform === 'darwin') {
|
||||||
|
console.warn(`⚠️ Warning: ${module.name} installation failed on macOS.`);
|
||||||
|
console.warn(' This is expected - macOS will fall back to environment variable or SSH key authentication.');
|
||||||
|
console.warn(' To enable PAM authentication, install Xcode Command Line Tools and rebuild.');
|
||||||
|
} else if (module.essential) {
|
||||||
|
console.error(`\n❌ ${module.name} is required for VibeTunnel to function.`);
|
||||||
|
console.error('You may need to install build tools for your platform:');
|
||||||
|
console.error('- macOS: Install Xcode Command Line Tools');
|
||||||
|
console.error('- Linux: Install build-essential and libpam0g-dev packages');
|
||||||
|
hasErrors = true;
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ Warning: ${module.name} installation failed. Some features may be limited.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install vt symlink/wrapper
|
||||||
|
if (!hasErrors && !isDevelopment) {
|
||||||
|
console.log('\nSetting up vt command...');
|
||||||
|
|
||||||
|
const vtSource = path.join(__dirname, '..', 'bin', 'vt');
|
||||||
|
|
||||||
|
// Check if vt script exists
|
||||||
|
if (!fs.existsSync(vtSource)) {
|
||||||
|
console.warn('⚠️ vt command script not found in package');
|
||||||
|
console.log(' Use "vibetunnel" command instead');
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// Make vt script executable
|
||||||
|
fs.chmodSync(vtSource, '755');
|
||||||
|
console.log('✓ vt command configured');
|
||||||
|
console.log(' Note: The vt command is available through npm/npx');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Could not configure vt command:', error.message);
|
||||||
|
console.log(' Use "vibetunnel" command instead');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasErrors) {
|
||||||
|
console.error('\n❌ Setup failed with errors');
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log('\n✅ VibeTunnel is ready to use');
|
||||||
|
console.log('Run "vibetunnel --help" for usage information');
|
||||||
|
}
|
||||||
45
web/scripts/test-npm-docker-verbose.sh
Executable file
45
web/scripts/test-npm-docker-verbose.sh
Executable file
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test VibeTunnel npm package installation with verbose output
|
||||||
|
|
||||||
|
VERSION=${1:-latest}
|
||||||
|
|
||||||
|
echo "Testing VibeTunnel npm package version: $VERSION"
|
||||||
|
echo "================================================"
|
||||||
|
|
||||||
|
# Create test directory
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
echo "Test directory: $TMP_DIR"
|
||||||
|
|
||||||
|
# Create Dockerfile
|
||||||
|
cat > "$TMP_DIR/Dockerfile" << 'EOF'
|
||||||
|
FROM node:20-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Test installation and PAM extraction
|
||||||
|
RUN echo "=== Installing VibeTunnel ===" && \
|
||||||
|
npm install -g vibetunnel@VERSION && \
|
||||||
|
echo "=== Installation complete ===" && \
|
||||||
|
echo "=== Checking for node_modules/authenticate-pam ===" && \
|
||||||
|
ls -la /usr/local/lib/node_modules/vibetunnel/node_modules/ | grep -E "(authenticate|optional)" || echo "No authenticate-pam in node_modules" && \
|
||||||
|
echo "=== Checking for optional-modules ===" && \
|
||||||
|
ls -la /usr/local/lib/node_modules/vibetunnel/optional-modules/ 2>/dev/null || echo "No optional-modules directory" && \
|
||||||
|
echo "=== Checking postinstall output ===" && \
|
||||||
|
cd /usr/local/lib/node_modules/vibetunnel && \
|
||||||
|
npm run postinstall || echo "Postinstall failed" && \
|
||||||
|
echo "=== Final check ===" && \
|
||||||
|
find /usr/local/lib/node_modules/vibetunnel -name "authenticate_pam.node" -type f 2>/dev/null || echo "No authenticate_pam.node found"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Replace VERSION placeholder
|
||||||
|
sed -i.bak "s/VERSION/$VERSION/g" "$TMP_DIR/Dockerfile"
|
||||||
|
|
||||||
|
# Build and run
|
||||||
|
echo "Building Docker image..."
|
||||||
|
docker build -t vibetunnel-npm-test-verbose "$TMP_DIR"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
|
||||||
|
echo "✅ Test complete!"
|
||||||
60
web/scripts/test-npm-docker.sh
Executable file
60
web/scripts/test-npm-docker.sh
Executable file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test VibeTunnel npm package installation in Docker
|
||||||
|
# Usage: ./scripts/test-npm-docker.sh [version]
|
||||||
|
# Example: ./scripts/test-npm-docker.sh 1.0.0-beta.11.4
|
||||||
|
|
||||||
|
VERSION=${1:-latest}
|
||||||
|
|
||||||
|
echo "Testing VibeTunnel npm package version: $VERSION"
|
||||||
|
echo "================================================"
|
||||||
|
|
||||||
|
# Create temporary dockerfile
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
DOCKERFILE="$TEMP_DIR/Dockerfile"
|
||||||
|
|
||||||
|
cat > "$DOCKERFILE" << EOF
|
||||||
|
FROM node:20-slim
|
||||||
|
|
||||||
|
# Test 1: Install without PAM headers (should succeed)
|
||||||
|
RUN echo "=== Test 1: Installing without PAM headers ===" && \
|
||||||
|
npm install -g vibetunnel@$VERSION && \
|
||||||
|
vibetunnel --version && \
|
||||||
|
node -e "try { require('authenticate-pam'); console.log('PAM available'); } catch { console.log('PAM not available - this is expected'); }"
|
||||||
|
|
||||||
|
# Test 2: Install PAM headers and check if module compiles
|
||||||
|
RUN echo "=== Test 2: Installing PAM headers ===" && \
|
||||||
|
apt-get update && apt-get install -y libpam0g-dev && \
|
||||||
|
echo "PAM headers installed"
|
||||||
|
|
||||||
|
# Test 3: Verify VibeTunnel still works
|
||||||
|
RUN echo "=== Test 3: Verifying VibeTunnel functionality ===" && \
|
||||||
|
vibetunnel --version && \
|
||||||
|
vibetunnel --help > /dev/null && \
|
||||||
|
echo "VibeTunnel is working correctly!"
|
||||||
|
|
||||||
|
CMD ["echo", "All tests passed successfully!"]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Build and run the test
|
||||||
|
echo "Building Docker image..."
|
||||||
|
docker build -f "$DOCKERFILE" -t vibetunnel-npm-test . || {
|
||||||
|
echo "❌ Docker build failed!"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Running tests..."
|
||||||
|
docker run --rm vibetunnel-npm-test || {
|
||||||
|
echo "❌ Tests failed!"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
docker rmi vibetunnel-npm-test > /dev/null 2>&1
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ All tests passed! VibeTunnel $VERSION installs correctly on Linux."
|
||||||
|
|
@ -77,17 +77,7 @@ if (fs.existsSync(seaPamPath) || fs.existsSync(seaNativePamPath)) {
|
||||||
|
|
||||||
// If normal require failed, try the optional-modules location
|
// If normal require failed, try the optional-modules location
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
const optionalModulePath = path.join(
|
const optionalModulePath = path.join(__dirname, '..', '..', '..', 'optional-modules', 'authenticate-pam', 'build', 'Release', 'authenticate_pam.node');
|
||||||
__dirname,
|
|
||||||
'..',
|
|
||||||
'..',
|
|
||||||
'..',
|
|
||||||
'optional-modules',
|
|
||||||
'authenticate-pam',
|
|
||||||
'build',
|
|
||||||
'Release',
|
|
||||||
'authenticate_pam.node'
|
|
||||||
);
|
|
||||||
if (fs.existsSync(optionalModulePath)) {
|
if (fs.existsSync(optionalModulePath)) {
|
||||||
try {
|
try {
|
||||||
const nativeModule = loadNativeModule(optionalModulePath);
|
const nativeModule = loadNativeModule(optionalModulePath);
|
||||||
|
|
|
||||||
29
web/test-vibetunnel-simple.dockerfile
Normal file
29
web/test-vibetunnel-simple.dockerfile
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Test VibeTunnel npm package with prebuilds
|
||||||
|
FROM node:22
|
||||||
|
|
||||||
|
# Create a test user
|
||||||
|
RUN useradd -m -s /bin/bash testuser
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install vibetunnel globally
|
||||||
|
RUN npm install -g --ignore-scripts vibetunnel@latest
|
||||||
|
|
||||||
|
# Test script
|
||||||
|
RUN echo '#!/bin/bash\n\
|
||||||
|
echo "Testing VibeTunnel npm package..."\n\
|
||||||
|
echo "Node version: $(node --version)"\n\
|
||||||
|
echo "npm version: $(npm --version)"\n\
|
||||||
|
echo ""\n\
|
||||||
|
echo "Testing vibetunnel command..."\n\
|
||||||
|
which vibetunnel && echo "✅ vibetunnel command found" || echo "❌ vibetunnel command not found"\n\
|
||||||
|
echo ""\n\
|
||||||
|
echo "Checking version..."\n\
|
||||||
|
vibetunnel --version 2>&1 || echo "Note: Version check may fail if native modules are missing"\n\
|
||||||
|
echo ""\n\
|
||||||
|
echo "Checking help..."\n\
|
||||||
|
vibetunnel --help 2>&1 | head -20\n\
|
||||||
|
' > /test.sh && chmod +x /test.sh
|
||||||
|
|
||||||
|
CMD ["/test.sh"]
|
||||||
26
web/test-vibetunnel-with-postinstall.dockerfile
Normal file
26
web/test-vibetunnel-with-postinstall.dockerfile
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Test VibeTunnel npm package with postinstall
|
||||||
|
FROM node:22
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install vibetunnel globally (with postinstall)
|
||||||
|
RUN npm install -g vibetunnel@latest
|
||||||
|
|
||||||
|
# Test script
|
||||||
|
RUN echo '#!/bin/bash\n\
|
||||||
|
echo "Testing VibeTunnel npm package..."\n\
|
||||||
|
echo "Node version: $(node --version)"\n\
|
||||||
|
echo ""\n\
|
||||||
|
echo "Checking installed files..."\n\
|
||||||
|
ls -la /usr/local/lib/node_modules/vibetunnel/node-pty/build/ 2>/dev/null || echo "No build directory"\n\
|
||||||
|
ls -la /usr/local/lib/node_modules/vibetunnel/prebuilds/ 2>/dev/null || echo "No prebuilds directory"\n\
|
||||||
|
echo ""\n\
|
||||||
|
echo "Testing vibetunnel command..."\n\
|
||||||
|
vibetunnel --version\n\
|
||||||
|
echo ""\n\
|
||||||
|
echo "Starting server test..."\n\
|
||||||
|
timeout 5 vibetunnel --port 4021 --no-auth || echo "Server test completed"\n\
|
||||||
|
' > /test.sh && chmod +x /test.sh
|
||||||
|
|
||||||
|
CMD ["/test.sh"]
|
||||||
43
web/test-vibetunnel.dockerfile
Normal file
43
web/test-vibetunnel.dockerfile
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Test VibeTunnel npm package on different Node.js versions
|
||||||
|
FROM node:22-slim
|
||||||
|
|
||||||
|
# Install dependencies for terminal functionality and building native modules
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
procps \
|
||||||
|
python3 \
|
||||||
|
build-essential \
|
||||||
|
libpam0g-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install vibetunnel globally as root
|
||||||
|
RUN npm install -g vibetunnel@latest
|
||||||
|
|
||||||
|
# Create a test user
|
||||||
|
RUN useradd -m -s /bin/bash testuser
|
||||||
|
|
||||||
|
# Switch to test user
|
||||||
|
USER testuser
|
||||||
|
WORKDIR /home/testuser
|
||||||
|
|
||||||
|
# Create a test script
|
||||||
|
RUN echo '#!/bin/bash\n\
|
||||||
|
echo "Testing VibeTunnel npm package..."\n\
|
||||||
|
echo "Node version: $(node --version)"\n\
|
||||||
|
echo "npm version: $(npm --version)"\n\
|
||||||
|
echo "VibeTunnel version:"\n\
|
||||||
|
vibetunnel --version\n\
|
||||||
|
echo ""\n\
|
||||||
|
echo "Starting VibeTunnel server..."\n\
|
||||||
|
vibetunnel --port 4021 --no-auth &\n\
|
||||||
|
SERVER_PID=$!\n\
|
||||||
|
sleep 3\n\
|
||||||
|
echo ""\n\
|
||||||
|
echo "Testing if server is running..."\n\
|
||||||
|
curl -s http://localhost:4021 > /dev/null && echo "✅ Server is responding" || echo "❌ Server not responding"\n\
|
||||||
|
echo ""\n\
|
||||||
|
echo "Stopping server..."\n\
|
||||||
|
kill $SERVER_PID\n\
|
||||||
|
' > test.sh && chmod +x test.sh
|
||||||
|
|
||||||
|
CMD ["./test.sh"]
|
||||||
Loading…
Reference in a new issue