* feat: Add script installation tracking with Container ID detection
- Add installed_scripts table to database schema
- Implement Container ID parsing from terminal output
- Add installation tracking for both local and SSH executions
- Create InstalledScriptsTab component with filtering and search
- Add tab navigation to main page (Scripts | Installed Scripts)
- Add tRPC endpoints for installed scripts CRUD operations
- Track installation status, server info, and output logs
- Support both local and SSH execution modes
* fix: Resolve SQL syntax error in database queries
- Change table alias from 'is' to 'inst' in SQL queries
- 'is' is a reserved keyword in SQLite causing syntax errors
- Fixes getAllInstalledScripts, getInstalledScriptById, and getInstalledScriptsByServer methods
* feat: Enhance Container ID detection and add manual editing
- Add comprehensive Container ID detection patterns for various script formats
- Add debug logging to help identify detection issues
- Add manual Container ID editing feature in the frontend
- Add updateInstalledScript tRPC mutation for updating records
- Improve Container ID column with inline editing UI
- Test and verify Container ID detection is working (detected 132 from 2fauth script)
* fix: Improve Container ID detection with ANSI code handling
- Add ANSI color code stripping before pattern matching
- Add primary pattern for exact format: 🆔 Container ID: 113
- Test patterns on both original and cleaned output
- Add better debug logging to show matched text
- This should fix Container ID detection for Proxmox scripts
* feat: Add script update functionality with terminal output
- Add Update button for each installed script (only shows when container_id exists)
- Add WebSocket support for update action (pct enter <ct-id> -- update)
- Add updateScript tRPC endpoint for initiating updates
- Add startScriptUpdate, startLocalScriptUpdate, startSSHScriptUpdate methods
- Modify Terminal component to handle update operations
- Display real-time terminal output for update commands
- Support both local and SSH execution modes for updates
- Show 'Update Container <ID>' in terminal title for update operations
* fix: Fix SSH update functionality
- Replace sshService.executeScript with direct sshpass command
- Use bash -c to execute SSH command: sshpass -p 'password' ssh -o StrictHostKeyChecking=no user@ip 'pct enter <ct-id> -- update'
- This fixes the 'Permission denied' and rsync errors
- SSH updates now work properly for remote containers
* fix: Fix WebSocket update action handling
- Add containerId to WebSocketMessage typedef
- Extract containerId from message in handleMessage function
- Remove debug logging from Terminal component
- This fixes the 'containerId is not defined' error
- Update action should now work properly without creating script records
* feat: Add Update functionality for installed scripts
- Add Update button to InstalledScriptsTab for scripts with Container ID
- Modify Terminal component to handle update operations with isUpdate and containerId props
- Add startUpdateExecution method to WebSocket handler
- Implement local update execution using 'pct enter <CT ID> -c update'
- Implement SSH update execution for remote servers
- Update WebSocket message parsing to handle update parameters
- Users can now update installed scripts by entering the LXC container and running update command
* fix: Fix SSH update execution by using direct command execution
- Add executeCommand method to SSH service for direct command execution
- Update startSSHUpdateExecution to use executeCommand instead of executeScript
- This fixes the rsync permission denied error when updating scripts via SSH
- Update functionality now works properly for both local and SSH installations
* fix: Add server credentials fetching for SSH updates
- Create servers router with getServerById endpoint
- Update handleUpdateScript to fetch full server details including credentials
- This fixes the permission denied error by providing user/password for SSH authentication
- SSH updates now have access to complete server configuration
* fix: Simplify server credentials fetching for SSH updates
- Add server_user and server_password to database query
- Update InstalledScript interface to include server credentials
- Simplify handleUpdateScript to use data already available
- Remove complex tRPC server fetching that was causing errors
- SSH updates now work with complete server authentication data
* fix: Correct pct enter command sequence for updates
- Change from 'pct enter <CT ID> -c "update"' to proper sequence
- First run 'pct enter <CT ID>' to enter container shell
- Then send 'update' command after entering the container
- Apply fix to both local and SSH update execution methods
- Add 1-second delay to ensure container shell is ready before sending update command
* fix: Increase delay to 4 seconds before sending update command
- Change delay from 1 second to 4 seconds for both local and SSH updates
- Ensures container shell is fully ready before sending update command
- Prevents premature command execution that could fail
* cleanup: Remove all debug console.log statements
- Remove debug logging from server.js WebSocket handlers
- Remove debug logging from Terminal component
- Remove debug logging from page.tsx
- Remove debug logging from ExecutionModeModal component
- Remove debug logging from githubJsonService.ts
- Keep only essential server startup messages and error logging
- Clean up codebase for production readiness
* fix: Resolve all build and linter errors
- Fix React Hook useEffect missing dependencies in Terminal.tsx
- Fix TypeScript unsafe argument error in installedScripts.ts by properly typing updateData
- Add missing isUpdate and containerId properties to WebSocketMessage type definition
- Add proper type annotations for callback parameters in server.js
- Fix TypeScript errors with execution.process by adding type assertions
- Remove duplicate updateInstalledScript method in installedScripts.ts
- Build now passes successfully with no errors or warnings
211 lines
6.5 KiB
JavaScript
211 lines
6.5 KiB
JavaScript
import { spawn } from 'child_process';
|
|
import { spawn as ptySpawn } from 'node-pty';
|
|
|
|
|
|
/**
|
|
* @typedef {Object} Server
|
|
* @property {string} ip - Server IP address
|
|
* @property {string} user - Username
|
|
* @property {string} password - Password
|
|
* @property {string} name - Server name
|
|
*/
|
|
|
|
class SSHExecutionService {
|
|
/**
|
|
* Execute a script on a remote server via SSH
|
|
* @param {Server} server - Server configuration
|
|
* @param {string} scriptPath - Path to the script
|
|
* @param {Function} onData - Callback for data output
|
|
* @param {Function} onError - Callback for errors
|
|
* @param {Function} onExit - Callback for process exit
|
|
* @returns {Promise<Object>} Process information
|
|
*/
|
|
async executeScript(server, scriptPath, onData, onError, onExit) {
|
|
const { ip, user, password } = server;
|
|
|
|
try {
|
|
await this.transferScriptsFolder(server, onData, onError);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const relativeScriptPath = scriptPath.startsWith('scripts/') ? scriptPath.substring(8) : scriptPath;
|
|
|
|
// Use ptySpawn for proper terminal emulation and color support
|
|
const sshCommand = ptySpawn('sshpass', [
|
|
'-p', password,
|
|
'ssh',
|
|
'-t',
|
|
'-o', 'ConnectTimeout=10',
|
|
'-o', 'StrictHostKeyChecking=no',
|
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
'-o', 'LogLevel=ERROR',
|
|
'-o', 'PasswordAuthentication=yes',
|
|
'-o', 'PubkeyAuthentication=no',
|
|
'-o', 'RequestTTY=yes',
|
|
'-o', 'SetEnv=TERM=xterm-256color',
|
|
'-o', 'SetEnv=COLUMNS=120',
|
|
'-o', 'SetEnv=LINES=30',
|
|
'-o', 'SetEnv=COLORTERM=truecolor',
|
|
'-o', 'SetEnv=FORCE_COLOR=1',
|
|
'-o', 'SetEnv=NO_COLOR=0',
|
|
'-o', 'SetEnv=CLICOLOR=1',
|
|
'-o', 'SetEnv=CLICOLOR_FORCE=1',
|
|
`${user}@${ip}`,
|
|
`cd /tmp/scripts && chmod +x ${relativeScriptPath} && export TERM=xterm-256color && export COLUMNS=120 && export LINES=30 && export COLORTERM=truecolor && export FORCE_COLOR=1 && export NO_COLOR=0 && export CLICOLOR=1 && export CLICOLOR_FORCE=1 && bash ${relativeScriptPath}`
|
|
], {
|
|
name: 'xterm-256color',
|
|
cols: 120,
|
|
rows: 30,
|
|
cwd: process.cwd(),
|
|
env: {
|
|
...process.env,
|
|
TERM: 'xterm-256color',
|
|
COLUMNS: '120',
|
|
LINES: '30',
|
|
SHELL: '/bin/bash',
|
|
COLORTERM: 'truecolor',
|
|
FORCE_COLOR: '1',
|
|
NO_COLOR: '0',
|
|
CLICOLOR: '1',
|
|
CLICOLOR_FORCE: '1'
|
|
}
|
|
});
|
|
|
|
// Use pty's onData method which handles both stdout and stderr combined
|
|
sshCommand.onData((data) => {
|
|
// pty handles encoding automatically and preserves ANSI codes
|
|
onData(data);
|
|
});
|
|
|
|
sshCommand.onExit((e) => {
|
|
onExit(e.exitCode);
|
|
});
|
|
|
|
resolve({
|
|
process: sshCommand,
|
|
kill: () => sshCommand.kill('SIGTERM')
|
|
});
|
|
});
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
onError(`SSH execution failed: ${errorMessage}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transfer the entire scripts folder to the remote server
|
|
* @param {Server} server - Server configuration
|
|
* @param {Function} onData - Callback for data output
|
|
* @param {Function} onError - Callback for errors
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async transferScriptsFolder(server, onData, onError) {
|
|
const { ip, user, password } = server;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const rsyncCommand = spawn('rsync', [
|
|
'-avz',
|
|
'--delete',
|
|
'--exclude=*.log',
|
|
'--exclude=*.tmp',
|
|
'--rsh=sshpass -p ' + password + ' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null',
|
|
'scripts/',
|
|
`${user}@${ip}:/tmp/scripts/`
|
|
], {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
rsyncCommand.stdout.on('data', (/** @type {Buffer} */ data) => {
|
|
// Ensure proper UTF-8 encoding for ANSI colors
|
|
const output = data.toString('utf8');
|
|
onData(output);
|
|
});
|
|
|
|
rsyncCommand.stderr.on('data', (/** @type {Buffer} */ data) => {
|
|
// Ensure proper UTF-8 encoding for ANSI colors
|
|
const output = data.toString('utf8');
|
|
onError(output);
|
|
});
|
|
|
|
rsyncCommand.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolve();
|
|
} else {
|
|
reject(new Error(`rsync failed with code ${code}`));
|
|
}
|
|
});
|
|
|
|
rsyncCommand.on('error', (error) => {
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Execute a direct command on a remote server via SSH
|
|
* @param {Server} server - Server configuration
|
|
* @param {string} command - Command to execute
|
|
* @param {Function} onData - Callback for data output
|
|
* @param {Function} onError - Callback for errors
|
|
* @param {Function} onExit - Callback for process exit
|
|
* @returns {Promise<Object>} Process information
|
|
*/
|
|
async executeCommand(server, command, onData, onError, onExit) {
|
|
const { ip, user, password } = server;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
// Use ptySpawn for proper terminal emulation and color support
|
|
const sshCommand = ptySpawn('sshpass', [
|
|
'-p', password,
|
|
'ssh',
|
|
'-t',
|
|
'-o', 'ConnectTimeout=10',
|
|
'-o', 'StrictHostKeyChecking=no',
|
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
'-o', 'LogLevel=ERROR',
|
|
'-o', 'PasswordAuthentication=yes',
|
|
'-o', 'PubkeyAuthentication=no',
|
|
'-o', 'RequestTTY=yes',
|
|
'-o', 'SetEnv=TERM=xterm-256color',
|
|
'-o', 'SetEnv=COLUMNS=120',
|
|
'-o', 'SetEnv=LINES=30',
|
|
'-o', 'SetEnv=COLORTERM=truecolor',
|
|
'-o', 'SetEnv=FORCE_COLOR=1',
|
|
'-o', 'SetEnv=NO_COLOR=0',
|
|
'-o', 'SetEnv=CLICOLOR=1',
|
|
`${user}@${ip}`,
|
|
command
|
|
], {
|
|
name: 'xterm-color',
|
|
cols: 120,
|
|
rows: 30,
|
|
cwd: process.cwd(),
|
|
env: process.env
|
|
});
|
|
|
|
sshCommand.onData((data) => {
|
|
onData(data);
|
|
});
|
|
|
|
sshCommand.onExit((e) => {
|
|
onExit(e.exitCode);
|
|
});
|
|
|
|
resolve({ process: sshCommand });
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
// Singleton instance
|
|
/** @type {SSHExecutionService | null} */
|
|
let sshExecutionInstance = null;
|
|
|
|
export function getSSHExecutionService() {
|
|
if (!sshExecutionInstance) {
|
|
sshExecutionInstance = new SSHExecutionService();
|
|
}
|
|
return sshExecutionInstance;
|
|
}
|
|
|
|
export default SSHExecutionService; |