eindelijk weer eens een push
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
FROM ubuntu:24.04
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Layer 1: OS packages - only rebuilds if this changes
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 python3-pip x3270 s3270 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Layer 2: Python deps - only rebuilds if requirements.txt changes
|
||||
COPY requirements.txt .
|
||||
RUN pip3 install --break-system-packages -r requirements.txt
|
||||
|
||||
# Layer 3: Python code - rebuilds on every code change
|
||||
COPY *.py .
|
||||
|
||||
EXPOSE 8000
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--log-level", "info"]
|
||||
@@ -0,0 +1,125 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from contextlib import asynccontextmanager
|
||||
from tn3270_service import TN3270Session
|
||||
import asyncio
|
||||
import logging
|
||||
import json
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
session = None
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
global session
|
||||
session = TN3270Session()
|
||||
try:
|
||||
session.connect()
|
||||
logger.info("TN3270 session ready")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect: {e}")
|
||||
yield
|
||||
if session:
|
||||
session.disconnect()
|
||||
|
||||
app = FastAPI(title="z/OS Web API", version="1.0.0", lifespan=lifespan)
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
class CommandRequest(BaseModel):
|
||||
text: str
|
||||
|
||||
def check():
|
||||
if not session or not session.connected:
|
||||
raise HTTPException(503, "Not connected to z/OS")
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "connected": session.connected if session else False}
|
||||
|
||||
@app.get("/screen")
|
||||
def get_screen():
|
||||
check()
|
||||
return {"screen": session.get_screen()}
|
||||
|
||||
@app.post("/command")
|
||||
def send_command(req: CommandRequest):
|
||||
check()
|
||||
return {"screen": session.send_command(req.text)}
|
||||
|
||||
@app.post("/enter")
|
||||
def send_enter():
|
||||
check()
|
||||
return {"screen": session.send_enter()}
|
||||
|
||||
@app.post("/pf/{key}")
|
||||
def send_pf(key: int):
|
||||
check()
|
||||
if not 1 <= key <= 24:
|
||||
raise HTTPException(400, "PF key must be 1-24")
|
||||
return {"screen": session.send_pf(key)}
|
||||
|
||||
@app.post("/tab")
|
||||
def send_tab():
|
||||
check()
|
||||
return {"screen": session.send_tab()}
|
||||
|
||||
class KeyRequest(BaseModel):
|
||||
key: str
|
||||
|
||||
class CharRequest(BaseModel):
|
||||
char: str
|
||||
|
||||
@app.post("/key")
|
||||
def send_key(req: KeyRequest):
|
||||
check()
|
||||
# Map key names to s3270 commands
|
||||
key_map = {
|
||||
"Tab": "Tab()",
|
||||
"BackTab": "BackTab()",
|
||||
"Backspace": "BackSpace()",
|
||||
"Delete": "Delete()",
|
||||
"Home": "Home()",
|
||||
"End": "FieldEnd()",
|
||||
"Insert": "Insert()",
|
||||
"Clear": "Clear()",
|
||||
"Attn": "Attn()",
|
||||
"PA1": "PA(1)",
|
||||
"PA2": "PA(2)",
|
||||
}
|
||||
s3270_key = key_map.get(req.key, req.key)
|
||||
return {"screen": session.send_key(s3270_key)}
|
||||
|
||||
@app.post("/char")
|
||||
def send_char(req: CharRequest):
|
||||
check()
|
||||
return {"screen": session.send_char(req.char)}
|
||||
|
||||
@app.get("/stream/screen")
|
||||
async def stream_screen():
|
||||
async def event_generator():
|
||||
last_screen = ""
|
||||
while True:
|
||||
try:
|
||||
if session and session.connected:
|
||||
if not session.lock.locked():
|
||||
screen = session.get_screen()
|
||||
if screen != last_screen:
|
||||
data = json.dumps({"screen": screen})
|
||||
yield f"data: {data}\n\n"
|
||||
last_screen = screen
|
||||
await asyncio.sleep(0.5)
|
||||
except Exception as e:
|
||||
logger.error(f"Stream error: {e}")
|
||||
break
|
||||
return StreamingResponse(
|
||||
event_generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"X-Accel-Buffering": "no",
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
fastapi==0.115.0
|
||||
uvicorn==0.30.6
|
||||
pydantic==2.9.2
|
||||
websockets==13.1
|
||||
@@ -0,0 +1,109 @@
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
import threading
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TN3270Session:
|
||||
def __init__(self):
|
||||
self.host = os.environ.get("ZOS_HOST", "192.168.2.243")
|
||||
self.port = os.environ.get("ZOS_PORT", "23")
|
||||
self.s3270 = None
|
||||
self.lock = threading.Lock()
|
||||
self.connected = False
|
||||
|
||||
def _start_s3270(self):
|
||||
self.s3270 = subprocess.Popen(
|
||||
["s3270", "-model", "3279-2", f"{self.host}:{self.port}"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
time.sleep(2)
|
||||
|
||||
def _send(self, command):
|
||||
self.s3270.stdin.write(command + "\n")
|
||||
self.s3270.stdin.flush()
|
||||
return self._read_response()
|
||||
|
||||
def _read_response(self):
|
||||
lines = []
|
||||
while True:
|
||||
line = self.s3270.stdout.readline()
|
||||
if not line:
|
||||
return False, lines
|
||||
line = line.rstrip("\n")
|
||||
if line == "ok":
|
||||
return True, lines
|
||||
if line == "error":
|
||||
return False, lines
|
||||
if line.startswith("data: "):
|
||||
line = line[6:]
|
||||
lines.append(line)
|
||||
|
||||
def connect(self):
|
||||
logger.info(f"Connecting to {self.host}:{self.port}")
|
||||
self._start_s3270()
|
||||
time.sleep(2)
|
||||
self.connected = True
|
||||
logger.info("TN3270 connected - waiting for user input")
|
||||
|
||||
def disconnect(self):
|
||||
try:
|
||||
self.s3270.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self.connected = False
|
||||
|
||||
def get_screen(self):
|
||||
with self.lock:
|
||||
ok, lines = self._send("Ascii()")
|
||||
return "\n".join(lines)
|
||||
|
||||
def send_command(self, command):
|
||||
with self.lock:
|
||||
self._send(f'String("{command}")')
|
||||
self._send("Enter()")
|
||||
time.sleep(0.5)
|
||||
ok, lines = self._send("Ascii()")
|
||||
return "\n".join(lines)
|
||||
|
||||
def send_pf(self, key):
|
||||
with self.lock:
|
||||
self._send(f"PF({key})")
|
||||
time.sleep(0.3)
|
||||
ok, lines = self._send("Ascii()")
|
||||
return "\n".join(lines)
|
||||
|
||||
def send_tab(self):
|
||||
with self.lock:
|
||||
self._send("Tab()")
|
||||
ok, lines = self._send("Ascii()")
|
||||
return "\n".join(lines)
|
||||
|
||||
def send_enter(self):
|
||||
with self.lock:
|
||||
self._send("Enter()")
|
||||
time.sleep(0.5)
|
||||
ok, lines = self._send("Ascii()")
|
||||
return "\n".join(lines)
|
||||
|
||||
def send_key(self, key):
|
||||
with self.lock:
|
||||
self._send(key)
|
||||
time.sleep(0.1)
|
||||
ok, lines = self._send("Ascii()")
|
||||
return "\n".join(lines)
|
||||
|
||||
def send_char(self, char):
|
||||
with self.lock:
|
||||
escaped = char.replace('"', '\"')
|
||||
self._send(f'String("{escaped}")')
|
||||
ok, lines = self._send("Ascii()")
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user