Skip to content

Commit c1893ff

Browse files
committed
Initial commit
0 parents  commit c1893ff

7 files changed

Lines changed: 485 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.DS_Store
2+
.idea

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FROM php:8.3-apache
2+
COPY src/ /var/www/html/

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Debug Shell in a Container
2+
3+
This project provides a simple yet powerful command-line interface within a containerized environment. It's designed
4+
for DevOps, Site Reliability Engineers (SREs), and engineers who need to poke around and debug applications,
5+
environments, or microservices in cloud-based or containerized setups such as Kubernetes, Cloud Run, and other
6+
serverless platforms.
7+
8+
## Features
9+
10+
- Access to a web-based terminal that mimics a typical command-line interface.
11+
- Supports common commands and streamed output (e.g., `tail -f`).
12+
- `Ctrl + C` for interrupting long-running commands.
13+
- Handles both `stdout` and `stderr` streaming.
14+
- Can be deployed in serverless environments like Google Cloud Run or Kubernetes to help debug multi-container setups or port exposure issues.
15+
16+
## Use Cases
17+
18+
- **Serverless Debugging**: Test, debug, and execute commands in environments like Cloud Run or Kubernetes.
19+
- **Cloud Run Multi-Container Debugging**: For example, this tool can help you verify that different containers within a Cloud Run multi-container deployment are responding correctly on the expected ports.
20+
- **Microservice Environments**: Use this tool to inspect and debug isolated services in distributed environments.
21+
- **Kubernetes Pods**: Get shell access to containers running in Kubernetes clusters to check logs, connectivity, and more.
22+
- **CI/CD Pipelines**: Debug issues directly in the environment where your code is running.
23+
24+
## Prerequisites
25+
26+
- Docker installed on your local machine.
27+
- Basic knowledge of Docker and container management.
28+
29+
## Installation
30+
31+
To build and run the container locally, follow these steps:
32+
33+
### Build the Docker Image
34+
35+
```bash
36+
docker build -t cmd .
37+
```
38+
39+
### Run the Docker Container
40+
41+
```bash
42+
docker run -d -p 8089:80 --name cmd cmd
43+
```
44+
45+
This will start the container and expose the terminal interface on port `8089` of your local machine. Access the
46+
interface by visiting `http://localhost:8089` in your browser.
47+
48+
## Usage
49+
Once the container is running, you can:
50+
51+
1. Visit http://localhost:8089 to access the web-based terminal.
52+
2. Use it like a normal terminal to run commands such as:
53+
- `ls`
54+
- `tail -f /path/to/logfile`
55+
- `apt-get update`
56+
- `curl http://example.com`
57+
3. Interrupt long-running commands with `Ctrl + C` to mimic real terminal behavior.
58+
59+
The terminal also supports streamed output, which is useful for commands like `tail -f` or to monitor logs or processes
60+
in real time.
61+
62+
### Debugging Cloud Run Multiple Container Deployments
63+
64+
You can use this tool to debug multi-container setups in serverless environments. For example, when deploying multiple
65+
containers on Google Cloud Run, use the terminal to ensure that services within the containers are properly exposed and
66+
responding on the correct ports.
67+
68+
Example:
69+
70+
```bash
71+
curl http://localhost:8080/api
72+
```
73+
74+
This allows you to validate whether your service is communicating with another container properly.
75+
76+
### Rebuilding the Container
77+
78+
If you need to make changes or rebuild the container, use the following commands:
79+
80+
```bash
81+
docker stop cmd
82+
docker rm cmd
83+
docker build -t cmd .
84+
docker run -d -p 8089:80 --name cmd cmd
85+
```
86+
87+
This will stop the currently running container, rebuild the image, and start a new container.
88+
89+
### Contributions
90+
Feel free to fork this repository and submit pull requests. This tool is designed to help others, and contributions
91+
that improve functionality or add new features are always welcome.
92+
93+
### License
94+
This project is open source and licensed under the MIT License. See the LICENSE file for more information.

src/execute.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
session_start();
4+
5+
header('Content-Type: text/plain');
6+
7+
$action = $_POST['action'] ?? 'run';
8+
$cwd = $_SESSION['cwd'] ?? getcwd();
9+
10+
function safe_flush() {
11+
if (ob_get_level() > 0) {
12+
ob_flush();
13+
flush();
14+
}
15+
}
16+
17+
function get_user_host_and_cwd() {
18+
$user = trim(exec('whoami'));
19+
$hostname = trim(gethostname());
20+
$cwd = trim(exec('pwd'));
21+
return [
22+
'user' => $user,
23+
'hostname' => $hostname,
24+
'cwd' => $cwd,
25+
];
26+
}
27+
28+
if ($action === 'initial') {
29+
$info = get_user_host_and_cwd();
30+
echo json_encode([
31+
'user' => $info['user'],
32+
'hostname' => $info['hostname'],
33+
'cwd' => $cwd,
34+
]) . "\n";
35+
exit;
36+
}
37+
38+
if ($action === 'run' && !empty($_POST['command'])) {
39+
$command = $_POST['command'];
40+
41+
if (preg_match('/^cd\s+(.+)$/', $command, $matches)) {
42+
$dir = $matches[1];
43+
if ($dir === '~') {
44+
$cwd = getenv('HOME');
45+
} else {
46+
$newCwd = realpath($cwd . '/' . $dir);
47+
if ($newCwd && is_dir($newCwd)) {
48+
$cwd = $newCwd;
49+
} else {
50+
echo json_encode(['output' => "bash: cd: $dir: No such file or directory"]) . "\n";
51+
safe_flush();
52+
exit;
53+
}
54+
}
55+
$_SESSION['cwd'] = $cwd;
56+
echo json_encode([
57+
'output' => '',
58+
'user' => exec('whoami'),
59+
'hostname' => gethostname(),
60+
'cwd' => $cwd,
61+
]) . "\n";
62+
safe_flush();
63+
exit;
64+
}
65+
66+
$descriptors = [
67+
1 => ['pipe', 'w'], // stdout
68+
2 => ['pipe', 'w'], // stderr
69+
];
70+
71+
$process = proc_open($command, $descriptors, $pipes, $cwd);
72+
73+
if (is_resource($process)) {
74+
$status = proc_get_status($process);
75+
$_SESSION['process'] = $status['pid'];
76+
77+
while (!feof($pipes[1]) || !feof($pipes[2])) {
78+
$stdoutLine = fgets($pipes[1]);
79+
if ($stdoutLine !== false) {
80+
echo json_encode(['output' => $stdoutLine]) . "\n";
81+
safe_flush();
82+
}
83+
84+
$stderrLine = fgets($pipes[2]);
85+
if ($stderrLine !== false) {
86+
echo json_encode(['output' => $stderrLine]) . "\n";
87+
safe_flush();
88+
}
89+
90+
if (connection_aborted()) {
91+
break;
92+
}
93+
}
94+
95+
fclose($pipes[1]);
96+
fclose($pipes[2]);
97+
98+
proc_close($process);
99+
unset($_SESSION['process']);
100+
}
101+
} elseif ($action === 'kill') {
102+
if (isset($_SESSION['process'])) {
103+
$pid = $_SESSION['process'];
104+
exec("kill -2 $pid");
105+
unset($_SESSION['process']);
106+
echo json_encode(['output' => "Process $pid interrupted"]) . "\n";
107+
} else {
108+
echo json_encode(['output' => 'No process running to kill']) . "\n";
109+
}
110+
}
111+
112+
session_write_close();

src/index.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Terminal</title>
7+
<link rel="stylesheet" href="style.css">
8+
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;700&display=swap" rel="stylesheet">
9+
</head>
10+
<body>
11+
<div class="terminal">
12+
<div class="terminal-header">
13+
<div class="buttons">
14+
<div class="close"></div>
15+
<div class="minimize"></div>
16+
<div class="maximize"></div>
17+
</div>
18+
<div class="terminal-title">bash - 80x24</div>
19+
</div>
20+
<div class="terminal-body" id="terminal-body"></div>
21+
</div>
22+
<script src="script.js"></script>
23+
</body>
24+
</html>

src/script.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
document.addEventListener('DOMContentLoaded', () => {
2+
const terminal = document.querySelector('.terminal');
3+
const maximizeButton = document.querySelector('.maximize');
4+
const terminalBody = document.getElementById('terminal-body');
5+
let currentUser = 'user'; // Default user, will be updated on page load
6+
let currentHostname = 'localhost'; // Default hostname, will be updated on page load
7+
let currentCwd = '~'; // Default working directory, will be updated on page load
8+
let streamRequest = null; // Track the current request for cancellation
9+
10+
// Maximize button functionality
11+
maximizeButton.addEventListener('click', () => {
12+
terminal.classList.toggle('terminal--fullscreen');
13+
});
14+
15+
// Function to fetch initial user and cwd
16+
function fetchInitialInfo() {
17+
const initialRequest = new XMLHttpRequest();
18+
initialRequest.open('POST', 'execute.php', true);
19+
initialRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
20+
initialRequest.onreadystatechange = function () {
21+
if (this.readyState === 4 && this.status === 200) {
22+
const response = JSON.parse(this.responseText);
23+
currentUser = response.user;
24+
currentHostname = response.hostname;
25+
currentCwd = response.cwd;
26+
addInputField(); // Show prompt after loading
27+
}
28+
};
29+
initialRequest.send('action=initial');
30+
}
31+
32+
// Function to send the command to the backend
33+
function processCommand(command) {
34+
const promptLine = document.createElement('div');
35+
promptLine.classList.add('output');
36+
promptLine.innerHTML = `<span class="prompt">${currentUser}@${currentHostname} ${currentCwd} %</span> ${command}`;
37+
terminalBody.appendChild(promptLine);
38+
39+
removeInputField();
40+
41+
streamRequest = new XMLHttpRequest();
42+
streamRequest.open('POST', 'execute.php', true);
43+
streamRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
44+
45+
let lastProcessedIndex = 0;
46+
47+
streamRequest.onreadystatechange = function () {
48+
if (this.readyState === 3 || this.readyState === 4) {
49+
const newData = this.responseText.substring(lastProcessedIndex);
50+
processResponse(newData);
51+
lastProcessedIndex = this.responseText.length;
52+
}
53+
54+
if (this.readyState === 4) {
55+
addInputField();
56+
}
57+
};
58+
59+
streamRequest.send(`command=${encodeURIComponent(command)}&action=run`);
60+
}
61+
62+
// Function to process the received response and output to the terminal
63+
function processResponse(responseText) {
64+
const lines = responseText.trim().split('\n');
65+
66+
lines.forEach((line) => {
67+
try {
68+
const response = JSON.parse(line);
69+
70+
if (response.user && response.cwd && response.hostname) {
71+
currentUser = response.user;
72+
currentHostname = response.hostname;
73+
currentCwd = response.cwd;
74+
}
75+
76+
if (response.output) {
77+
const resultLine = document.createElement('div');
78+
resultLine.classList.add('output');
79+
resultLine.innerHTML = response.output.replace(/\n/g, '<br>');
80+
terminalBody.appendChild(resultLine);
81+
terminalBody.scrollTop = terminalBody.scrollHeight;
82+
}
83+
} catch (e) {
84+
console.error("Error parsing JSON: ", line);
85+
}
86+
});
87+
}
88+
89+
function addInputField() {
90+
const inputLine = document.createElement('div');
91+
inputLine.classList.add('input-line');
92+
inputLine.innerHTML = `<span class="prompt">${currentUser}@${currentHostname} ${currentCwd} %</span> <input type="text" class="input" id="terminal-input" autofocus>`;
93+
94+
terminalBody.appendChild(inputLine);
95+
terminalBody.scrollTop = terminalBody.scrollHeight;
96+
97+
const newInput = document.getElementById('terminal-input');
98+
newInput.focus();
99+
newInput.addEventListener('keydown', (e) => {
100+
if (e.key === 'Enter') {
101+
const command = newInput.value.trim();
102+
if (command) {
103+
processCommand(command);
104+
}
105+
newInput.value = '';
106+
}
107+
if (e.key === 'c' && (e.ctrlKey || e.metaKey)) {
108+
killProcess();
109+
}
110+
});
111+
}
112+
113+
function removeInputField() {
114+
const existingInput = document.querySelector('.input-line');
115+
if (existingInput) {
116+
terminalBody.removeChild(existingInput);
117+
}
118+
}
119+
120+
function killProcess() {
121+
if (streamRequest) {
122+
const killRequest = new XMLHttpRequest();
123+
killRequest.open('POST', 'execute.php', true);
124+
killRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
125+
killRequest.onreadystatechange = function () {
126+
if (this.readyState === 4 && this.status === 200) {
127+
const response = JSON.parse(this.responseText);
128+
const resultLine = document.createElement('div');
129+
resultLine.classList.add('output');
130+
resultLine.innerHTML = `<span style="color: red;">${response.output}</span>`;
131+
terminalBody.appendChild(resultLine);
132+
terminalBody.scrollTop = terminalBody.scrollHeight;
133+
addInputField();
134+
}
135+
};
136+
killRequest.send('action=kill');
137+
}
138+
}
139+
140+
// Fetch initial info and add input field
141+
fetchInitialInfo();
142+
});

0 commit comments

Comments
 (0)