Goblin Demo¶
Step-by-step walkthrough using the synchronous helpers.
In [1]:
Copied!
import os
import select
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from pdum.criu import goblins
print("Demo imports ready.")
import os
import select
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from pdum.criu import goblins
print("Demo imports ready.")
Demo imports ready.
In [2]:
Copied!
GOBLIN_PAYLOAD = r"""
import os
import sys
print(f"Goblin PID={os.getpid()} ready", flush=True)
for line in sys.stdin:
text = line.rstrip("\n")
if text == "":
print(f"[{os.getpid()}] (noop)", flush=True)
continue
print(f"[{os.getpid()}] echo: {text}", flush=True)
sys.stdout.flush()
print(f"[{os.getpid()}] stdin closed, exiting", flush=True)
"""
print("Payload defined.")
GOBLIN_PAYLOAD = r"""
import os
import sys
print(f"Goblin PID={os.getpid()} ready", flush=True)
for line in sys.stdin:
text = line.rstrip("\n")
if text == "":
print(f"[{os.getpid()}] (noop)", flush=True)
continue
print(f"[{os.getpid()}] echo: {text}", flush=True)
sys.stdout.flush()
print(f"[{os.getpid()}] stdin closed, exiting", flush=True)
"""
print("Payload defined.")
Payload defined.
In [3]:
Copied!
def _write_line(writer, text: str) -> None:
data = (text.rstrip("\n") + "\n").encode("utf-8")
writer.write(data)
writer.flush()
def _read_line(reader, *, timeout: float = 5.0) -> str:
fd = reader.fileno()
deadline = time.time() + timeout
while True:
remaining = deadline - time.time()
if remaining <= 0:
raise TimeoutError("timed out waiting for goblin output")
ready, _, _ = select.select([fd], [], [], remaining)
if not ready:
continue
line = reader.readline()
if not line:
return ""
return line.decode("utf-8", errors="replace").rstrip("\n")
def _drain(reader) -> None:
if reader.closed:
return
fd = reader.fileno()
while True:
ready, _, _ = select.select([fd], [], [], 0)
if not ready:
break
chunk = reader.readline()
if not chunk:
break
print(f"[stderr] {chunk.decode('utf-8', errors='replace').rstrip()}")
def _launch_goblin(python: str) -> subprocess.Popen:
proc = subprocess.Popen(
[python, "-u", "-c", GOBLIN_PAYLOAD],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True,
)
if proc.stdout is None or proc.stdin is None or proc.stderr is None:
raise RuntimeError("failed to capture goblin stdio pipes")
banner = _read_line(proc.stdout)
print(f"Original goblin says: {banner}")
return proc
def _wait_for_pid(pid: int, timeout: float = 5.0) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
try:
os.kill(pid, 0)
except OSError:
print(f"Process {pid} exited")
return
time.sleep(0.1)
print(f"Process {pid} still running after {timeout:.1f}s")
def _wait_for_pidfile(pidfile: Path, timeout: float = 5.0) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
try:
int(Path(pidfile).read_text().strip())
return
except (FileNotFoundError, ValueError):
time.sleep(0.02)
raise TimeoutError(f"timed out waiting for pidfile {pidfile}")
def _write_line(writer, text: str) -> None:
data = (text.rstrip("\n") + "\n").encode("utf-8")
writer.write(data)
writer.flush()
def _read_line(reader, *, timeout: float = 5.0) -> str:
fd = reader.fileno()
deadline = time.time() + timeout
while True:
remaining = deadline - time.time()
if remaining <= 0:
raise TimeoutError("timed out waiting for goblin output")
ready, _, _ = select.select([fd], [], [], remaining)
if not ready:
continue
line = reader.readline()
if not line:
return ""
return line.decode("utf-8", errors="replace").rstrip("\n")
def _drain(reader) -> None:
if reader.closed:
return
fd = reader.fileno()
while True:
ready, _, _ = select.select([fd], [], [], 0)
if not ready:
break
chunk = reader.readline()
if not chunk:
break
print(f"[stderr] {chunk.decode('utf-8', errors='replace').rstrip()}")
def _launch_goblin(python: str) -> subprocess.Popen:
proc = subprocess.Popen(
[python, "-u", "-c", GOBLIN_PAYLOAD],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True,
)
if proc.stdout is None or proc.stdin is None or proc.stderr is None:
raise RuntimeError("failed to capture goblin stdio pipes")
banner = _read_line(proc.stdout)
print(f"Original goblin says: {banner}")
return proc
def _wait_for_pid(pid: int, timeout: float = 5.0) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
try:
os.kill(pid, 0)
except OSError:
print(f"Process {pid} exited")
return
time.sleep(0.1)
print(f"Process {pid} still running after {timeout:.1f}s")
def _wait_for_pidfile(pidfile: Path, timeout: float = 5.0) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
try:
int(Path(pidfile).read_text().strip())
return
except (FileNotFoundError, ValueError):
time.sleep(0.02)
raise TimeoutError(f"timed out waiting for pidfile {pidfile}")
In [4]:
Copied!
sync_dir = Path(tempfile.mkdtemp(prefix="goblin-sync-demo-"))
python_bin = sys.executable
proc = _launch_goblin(python_bin)
assert proc.stdin and proc.stdout and proc.stderr
print(f"Sync images dir: {sync_dir}")
sync_dir = Path(tempfile.mkdtemp(prefix="goblin-sync-demo-"))
python_bin = sys.executable
proc = _launch_goblin(python_bin)
assert proc.stdin and proc.stdout and proc.stderr
print(f"Sync images dir: {sync_dir}")
Original goblin says: Goblin PID=463662 ready Sync images dir: /tmp/goblin-sync-demo-36iv7kaj
In [5]:
Copied!
_write_line(proc.stdin, "hello before freeze")
print("Original response:", _read_line(proc.stdout))
log_path = goblins.freeze(proc.pid, sync_dir, leave_running=True, verbosity=4, shell_job=False)
print(f"Goblin frozen into {sync_dir} (log {log_path})")
_write_line(proc.stdin, "hello before freeze")
print("Original response:", _read_line(proc.stdout))
log_path = goblins.freeze(proc.pid, sync_dir, leave_running=True, verbosity=4, shell_job=False)
print(f"Goblin frozen into {sync_dir} (log {log_path})")
Original response: [463662] echo: hello before freeze Goblin frozen into /tmp/goblin-sync-demo-36iv7kaj (log /tmp/goblin-sync-demo-36iv7kaj/goblin-freeze.463662.log)
In [6]:
Copied!
thawed = goblins.thaw(sync_dir, shell_job=False)
_wait_for_pidfile(thawed.pidfile)
print(f"Thawed goblin PID={thawed.read_pidfile()} (original PID={proc.pid}, restore helper PID={thawed.helper_pid})")
_write_line(proc.stdin, "original still alive")
print("Original response:", _read_line(proc.stdout))
_write_line(thawed.stdin, "hello from thawed client")
print("Thawed response:", _read_line(thawed.stdout))
thawed = goblins.thaw(sync_dir, shell_job=False)
_wait_for_pidfile(thawed.pidfile)
print(f"Thawed goblin PID={thawed.read_pidfile()} (original PID={proc.pid}, restore helper PID={thawed.helper_pid})")
_write_line(proc.stdin, "original still alive")
print("Original response:", _read_line(proc.stdout))
_write_line(thawed.stdin, "hello from thawed client")
print("Thawed response:", _read_line(thawed.stdout))
Thawed goblin PID=463681 (original PID=463662, restore helper PID=463675) Original response: [463662] echo: original still alive Thawed response: [463662] echo: hello from thawed client
In [7]:
Copied!
_write_line(proc.stdin, "orig second ping")
_write_line(thawed.stdin, "thawed second ping")
print("Original second response:", _read_line(proc.stdout))
print("Thawed second response:", _read_line(thawed.stdout))
_write_line(proc.stdin, "orig second ping")
_write_line(thawed.stdin, "thawed second ping")
print("Original second response:", _read_line(proc.stdout))
print("Thawed second response:", _read_line(thawed.stdout))
Original second response: [463662] echo: orig second ping Thawed second response: [463662] echo: thawed second ping
In [8]:
Copied!
proc.stdin.close()
thawed.stdin.close()
try:
print("Original exit message:", _read_line(proc.stdout, timeout=2))
except TimeoutError:
print("Original goblin did not exit on cue")
try:
print("Thawed exit message:", _read_line(thawed.stdout, timeout=2))
except TimeoutError:
print("Thawed goblin did not exit on cue")
thawed.close()
if thawed.helper_pid:
_wait_for_pid(thawed.helper_pid, timeout=5)
proc.wait(timeout=5)
_drain(proc.stderr)
print("Sync demo complete.")
proc.stdin.close()
thawed.stdin.close()
try:
print("Original exit message:", _read_line(proc.stdout, timeout=2))
except TimeoutError:
print("Original goblin did not exit on cue")
try:
print("Thawed exit message:", _read_line(thawed.stdout, timeout=2))
except TimeoutError:
print("Thawed goblin did not exit on cue")
thawed.close()
if thawed.helper_pid:
_wait_for_pid(thawed.helper_pid, timeout=5)
proc.wait(timeout=5)
_drain(proc.stderr)
print("Sync demo complete.")
Original exit message: [463662] stdin closed, exiting Thawed exit message: [463662] stdin closed, exiting Process 463675 exited Sync demo complete.
Goblin Async Demo¶
Equivalent walkthrough using asyncio helpers (top-level await).
In [9]:
Copied!
import asyncio
print("Async demo imports ready.")
import asyncio
print("Async demo imports ready.")
Async demo imports ready.
In [10]:
Copied!
ASYNC_PAYLOAD = GOBLIN_PAYLOAD
print("Async payload mirrors sync payload.")
ASYNC_PAYLOAD = GOBLIN_PAYLOAD
print("Async payload mirrors sync payload.")
Async payload mirrors sync payload.
In [11]:
Copied!
async def _async_write_line(writer: asyncio.StreamWriter, text: str) -> None:
writer.write((text.rstrip("\n") + "\n").encode("utf-8"))
await writer.drain()
async def _async_read_line(reader: asyncio.StreamReader, timeout: float = 5.0) -> str:
try:
line = await asyncio.wait_for(reader.readline(), timeout=timeout)
except asyncio.TimeoutError as exc:
raise TimeoutError("timed out waiting for goblin output") from exc
return line.decode("utf-8", errors="replace").rstrip("\n")
async def _async_launch_goblin(python: str) -> asyncio.subprocess.Process:
proc = await asyncio.create_subprocess_exec(
python,
"-u",
"-c",
ASYNC_PAYLOAD,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
start_new_session=True,
)
assert proc.stdout is not None
banner = await _async_read_line(proc.stdout)
print(f"Original goblin says: {banner}")
return proc
async def _async_wait_for_pid(pid: int, timeout: float = 5.0) -> None:
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while loop.time() < deadline:
try:
os.kill(pid, 0)
except OSError:
print(f"Process {pid} exited")
return
await asyncio.sleep(0.1)
print(f"Process {pid} still running after {timeout:.1f}s")
async def _async_wait_for_pidfile(pidfile: Path, timeout: float = 5.0) -> None:
loop = asyncio.get_event_loop()
deadline = loop.time() + timeout
while True:
try:
int(Path(pidfile).read_text().strip())
return
except (FileNotFoundError, ValueError):
pass
remaining = deadline - loop.time()
if remaining <= 0:
raise TimeoutError(f"timed out waiting for pidfile {pidfile}")
await asyncio.sleep(min(0.05, remaining))
async def _async_write_line(writer: asyncio.StreamWriter, text: str) -> None:
writer.write((text.rstrip("\n") + "\n").encode("utf-8"))
await writer.drain()
async def _async_read_line(reader: asyncio.StreamReader, timeout: float = 5.0) -> str:
try:
line = await asyncio.wait_for(reader.readline(), timeout=timeout)
except asyncio.TimeoutError as exc:
raise TimeoutError("timed out waiting for goblin output") from exc
return line.decode("utf-8", errors="replace").rstrip("\n")
async def _async_launch_goblin(python: str) -> asyncio.subprocess.Process:
proc = await asyncio.create_subprocess_exec(
python,
"-u",
"-c",
ASYNC_PAYLOAD,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
start_new_session=True,
)
assert proc.stdout is not None
banner = await _async_read_line(proc.stdout)
print(f"Original goblin says: {banner}")
return proc
async def _async_wait_for_pid(pid: int, timeout: float = 5.0) -> None:
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while loop.time() < deadline:
try:
os.kill(pid, 0)
except OSError:
print(f"Process {pid} exited")
return
await asyncio.sleep(0.1)
print(f"Process {pid} still running after {timeout:.1f}s")
async def _async_wait_for_pidfile(pidfile: Path, timeout: float = 5.0) -> None:
loop = asyncio.get_event_loop()
deadline = loop.time() + timeout
while True:
try:
int(Path(pidfile).read_text().strip())
return
except (FileNotFoundError, ValueError):
pass
remaining = deadline - loop.time()
if remaining <= 0:
raise TimeoutError(f"timed out waiting for pidfile {pidfile}")
await asyncio.sleep(min(0.05, remaining))
In [12]:
Copied!
async_dir = Path(tempfile.mkdtemp(prefix="goblin-async-demo-"))
python_bin = sys.executable
print(f"Async images dir: {async_dir}")
async_dir = Path(tempfile.mkdtemp(prefix="goblin-async-demo-"))
python_bin = sys.executable
print(f"Async images dir: {async_dir}")
Async images dir: /tmp/goblin-async-demo-ntmj0nry
In [13]:
Copied!
proc = await _async_launch_goblin(python_bin)
assert proc.stdin and proc.stdout
proc = await _async_launch_goblin(python_bin)
assert proc.stdin and proc.stdout
Original goblin says: Goblin PID=463682 ready
In [14]:
Copied!
await _async_write_line(proc.stdin, "hello before freeze")
print("Original response:", await _async_read_line(proc.stdout))
await goblins.freeze_async(proc.pid, async_dir, leave_running=True, verbosity=4, shell_job=False)
print(f"Goblin frozen into {async_dir}")
await _async_write_line(proc.stdin, "hello before freeze")
print("Original response:", await _async_read_line(proc.stdout))
await goblins.freeze_async(proc.pid, async_dir, leave_running=True, verbosity=4, shell_job=False)
print(f"Goblin frozen into {async_dir}")
Original response: [463682] echo: hello before freeze
Goblin frozen into /tmp/goblin-async-demo-ntmj0nry
In [15]:
Copied!
thawed = await goblins.thaw_async(async_dir, shell_job=False)
await _async_wait_for_pidfile(thawed.pidfile)
print(f"Thawed goblin PID={await thawed.read_pidfile()} (original PID={proc.pid})")
await _async_write_line(proc.stdin, "original still alive")
print("Original response:", await _async_read_line(proc.stdout))
await _async_write_line(thawed.stdin, "hello from thawed client")
print("Thawed response:", await _async_read_line(thawed.stdout))
thawed = await goblins.thaw_async(async_dir, shell_job=False)
await _async_wait_for_pidfile(thawed.pidfile)
print(f"Thawed goblin PID={await thawed.read_pidfile()} (original PID={proc.pid})")
await _async_write_line(proc.stdin, "original still alive")
print("Original response:", await _async_read_line(proc.stdout))
await _async_write_line(thawed.stdin, "hello from thawed client")
print("Thawed response:", await _async_read_line(thawed.stdout))
Thawed goblin PID=463698 (original PID=463682) Original response: [463682] echo: original still alive Thawed response: [463682] echo: hello from thawed client
In [16]:
Copied!
await _async_write_line(proc.stdin, "orig second ping")
await _async_write_line(thawed.stdin, "thawed second ping")
print("Original second response:", await _async_read_line(proc.stdout))
print("Thawed second response:", await _async_read_line(thawed.stdout))
await _async_write_line(proc.stdin, "orig second ping")
await _async_write_line(thawed.stdin, "thawed second ping")
print("Original second response:", await _async_read_line(proc.stdout))
print("Thawed second response:", await _async_read_line(thawed.stdout))
Original second response: [463682] echo: orig second ping Thawed second response: [463682] echo: thawed second ping
In [17]:
Copied!
await _async_write_line(proc.stdin, "exit")
await _async_write_line(thawed.stdin, "exit")
proc.stdin.close()
thawed.stdin.close()
await _async_write_line(proc.stdin, "exit")
await _async_write_line(thawed.stdin, "exit")
proc.stdin.close()
thawed.stdin.close()
In [18]:
Copied!
try:
print("Original exit message:", await _async_read_line(proc.stdout, timeout=2))
except TimeoutError:
print("Original goblin did not exit on cue")
try:
print("Thawed exit message:", await _async_read_line(thawed.stdout, timeout=5))
except TimeoutError:
print("Thawed goblin did not exit on cue")
try:
print("Original exit message:", await _async_read_line(proc.stdout, timeout=2))
except TimeoutError:
print("Original goblin did not exit on cue")
try:
print("Thawed exit message:", await _async_read_line(thawed.stdout, timeout=5))
except TimeoutError:
print("Thawed goblin did not exit on cue")
Original exit message: [463682] echo: exit Thawed exit message: [463682] echo: exit
In [19]:
Copied!
await thawed.close()
if thawed.helper_pid:
await _async_wait_for_pid(thawed.helper_pid, timeout=5)
await proc.wait()
print("Async demo complete.")
await thawed.close()
if thawed.helper_pid:
await _async_wait_for_pid(thawed.helper_pid, timeout=5)
await proc.wait()
print("Async demo complete.")
Process 463693 still running after 5.0s Async demo complete.