-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathimmortal.html
More file actions
208 lines (195 loc) · 10.4 KB
/
Copy pathimmortal.html
File metadata and controls
208 lines (195 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Immortal Python Machine - pyproc</title>
<link rel="icon" href="../assets/logo.svg">
<meta name="theme-color" content="#0a0f1c">
<link rel="stylesheet" href="demo.css">
<script type="module" src="siteChrome.js"></script>
<style>
.kernelGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(12rem,1fr));gap:.7rem;margin-top:.8rem}
.kernelStat{background:var(--deep);border:1px solid var(--border);border-radius:9px;padding:.65rem .75rem}
.kernelStat b{display:block;color:var(--dim);font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;margin-bottom:.15rem}
.kernelStat span{overflow-wrap:anywhere}
.steps{margin:.65rem 0 0;padding-left:1.25rem;color:var(--dim)}
.steps li{margin:.25rem 0}
</style>
</head>
<body>
<div class="wrap">
<header class="site">
<a class="logo" href="../"><img class="logoMark" src="../assets/logo.svg" width="22" height="22" alt=""> <span><span class="py">py</span>proc</span></a>
<nav>
<a href="immortal.html" class="on">Immortal</a>
<a href="agentSandbox.html">Agent</a>
<a href="machine.html">Machine</a>
<a href="serverDev.html">Server</a>
<a href="speedLab.html">Speed</a>
<a href="terminal.html">Terminal</a>
<a href="processOs.html">Process OS</a>
<a href="basic.html">Basics</a>
</nav>
<sns-links></sns-links>
</header>
<h1>Close the tab. Your Python machine is still there.</h1>
<p class="lead">Every tab with this machine name is a view onto one local Python state. One tab is the leader.
If it closes, another tab takes over from the last committed heap and <span class="ok">/home/web</span> files.
No backend, no remote Python.</p>
<div class="panel">
<div class="status" id="headline">Opening the persistent machine...</div>
<div class="kernelGrid">
<div class="kernelStat"><b>Machine</b><span id="machineName">-</span></div>
<div class="kernelStat"><b>This tab</b><span id="participantId">-</span></div>
<div class="kernelStat"><b>Role</b><span id="role">-</span></div>
<div class="kernelStat"><b>Leader</b><span id="leaderId">-</span></div>
<div class="kernelStat"><b>Epoch</b><span id="epoch">-</span></div>
<div class="kernelStat"><b>Participants</b><span id="participantCount">-</span></div>
<div class="kernelStat"><b>Recovered</b><span id="recovered">-</span></div>
<div class="kernelStat"><b>Last commit</b><span id="lastCommitAt">-</span></div>
<div class="kernelStat"><b>Takeover</b><span id="failoverMs">not observed</span></div>
<div class="kernelStat"><b>Browser lane</b><span id="browserLane">-</span></div>
</div>
</div>
<div class="panel">
<p class="dim" style="margin:0 0 .55rem">The command below changes both Python memory and a file. Run it, commit, open another participant, then close the tab marked leader.</p>
<textarea id="code" rows="5" spellcheck="false">import os
os.makedirs('/home/web/immortal', exist_ok=True)
counter = globals().get('counter', 40) + 1
open('/home/web/immortal/counter.txt', 'w').write(str(counter))
f'counter={counter}, file={open("/home/web/immortal/counter.txt").read()}'</textarea>
<div class="row">
<button id="run" disabled>Run Python</button>
<button id="commit" class="ghost" disabled>Commit memory + files</button>
<button id="newTab" class="ghost">Open participant tab</button>
<button id="closeLeader" class="ghost" disabled>Close this leader tab</button>
</div>
<div class="term" id="out">Waiting for a leader...</div>
</div>
<div class="panel">
<strong>Try the takeover</strong>
<ol class="steps">
<li>Run the sample. It creates <code>counter = 41</code> and a file.</li>
<li>Commit. This is the exact crash-recovery boundary.</li>
<li>Open a participant tab. Both tabs show the same leader and epoch.</li>
<li>Close the tab whose role is <code>leader</code>.</li>
<li>The survivor advances the epoch, reports <code>recovered = yes</code>, and can run <code>counter + 1</code>.</li>
<li>Close every tab and reopen this page. The last commit still returns.</li>
</ol>
<p class="dim" style="margin:.65rem 0 0">Recovery is exact at a completed commit boundary. In-flight Promises, requests, sockets, and an arbitrary Python stack are not resurrected. A request interrupted after transmission returns an outcome-unknown error and is never silently replayed.</p>
</div>
<footer class="site">Chromium/Edge. Source: <a href="https://github.com/eddmpython/pyproc">github.com/eddmpython/pyproc</a> · <a href="https://www.npmjs.com/package/pyproc">npm install pyproc</a></footer>
</div>
<script type="module">
import { open } from "../index.js";
const elements = Object.fromEntries([
"headline", "machineName", "participantId", "role", "leaderId", "epoch", "participantCount",
"recovered", "lastCommitAt", "failoverMs", "browserLane", "code", "run", "commit", "newTab",
"closeLeader", "out",
].map((id) => [id, document.getElementById(id)]));
const params = new URLSearchParams(location.search);
const gateMode = params.has("gate");
const INDEX = params.get("indexURL") ? new URL(params.get("indexURL"), location.href).href : undefined;
const machineName = params.get("machine") || "pyprocImmortalDemo";
const short = (value) => value ? String(value).slice(0, 8) : "-";
const print = (text) => {
elements.out.textContent += (elements.out.textContent ? "\n" : "") + text;
elements.out.scrollTop = elements.out.scrollHeight;
};
const gateReport = (ok, info) => {
if (!gateMode) return;
fetch("/gateReport", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ok: !!ok, checks: [{ name: document.title, pass: !!ok, info: String(info).slice(-220) }] }),
}).catch(() => {});
};
let machine = null;
let previousLeader = null;
let takeoverStarted = null;
const render = (status) => {
if (previousLeader && status.leaderId && previousLeader !== status.leaderId && status.phase === "recovering") takeoverStarted = performance.now();
if (takeoverStarted && status.phase === "ready") {
elements.failoverMs.textContent = `${Math.round(performance.now() - takeoverStarted)}ms`;
takeoverStarted = null;
}
if (status.leaderId) previousLeader = status.leaderId;
elements.machineName.textContent = status.name;
elements.participantId.textContent = short(status.participantId);
elements.role.textContent = `${status.role} / ${status.phase}`;
elements.leaderId.textContent = short(status.leaderId);
elements.epoch.textContent = String(status.epoch);
elements.participantCount.textContent = String(status.participantCount);
elements.recovered.textContent = status.recovered ? "yes" : "no";
elements.lastCommitAt.textContent = status.lastCommitAt ? new Date(status.lastCommitAt).toLocaleTimeString() : "not committed";
elements.browserLane.textContent = status.crossOriginIsolated ? "COI + SAB + JSPI" : "JSPI, SAB unavailable";
elements.headline.textContent = status.phase === "ready"
? `${status.role === "leader" ? "Serving" : "Attached"}: epoch ${status.epoch}, leader ${short(status.leaderId)}`
: `${status.phase}: waiting for the Python machine`;
elements.run.disabled = status.phase !== "ready";
elements.commit.disabled = status.phase !== "ready";
elements.closeLeader.disabled = status.role !== "leader" || status.phase !== "ready";
};
try {
// open({ persistent }) = 멀티탭 영속 머신: Web Locks 선출 + 저널 부활(KernelElection 핸들).
machine = await open({
persistent: {
name: machineName,
manifest: { indexURL: INDEX },
onStatus: render,
},
});
render(machine.status());
elements.out.textContent = machine.status().recovered
? "Recovered the last committed Python memory and /home/web files."
: "Fresh machine. Run the sample, then commit it.";
const execute = async () => {
const result = await machine.run(elements.code.value, { async: true });
print(`>>> result\n${String(result)}`);
return result;
};
const commit = async () => {
const result = await machine.commit();
print(`commit: ${result.pages} heap pages, ${result.home?.files || 0} home files, ${result.committedAt}`);
render(machine.status());
return result;
};
if (gateMode) {
const result = await execute();
const committed = await commit();
const status = machine.status();
gateReport(result === "counter=41, file=41" && !!committed.committedAt && committed.home?.files >= 1 && status.phase === "ready",
`${result} | ${status.role} epoch=${status.epoch} recovered=${status.recovered}`);
}
elements.run.addEventListener("click", async () => {
elements.run.disabled = true;
try { await execute(); }
catch (error) { print(`${error.code || "error"}: ${error.message}`); }
finally { elements.run.disabled = false; }
});
elements.commit.addEventListener("click", async () => {
elements.commit.disabled = true;
try { await commit(); }
catch (error) { print(`${error.code || "error"}: ${error.message}`); }
finally { elements.commit.disabled = false; }
});
} catch (error) {
elements.headline.textContent = "Could not open the persistent machine.";
print(String(error.stack || error));
gateReport(false, error.message || error);
}
elements.newTab.addEventListener("click", () => {
const url = new URL(location.href);
url.searchParams.delete("gate");
url.searchParams.set("machine", machineName);
window.open(url, "_blank", "noopener=false");
});
elements.closeLeader.addEventListener("click", () => {
window.close();
setTimeout(() => print("This browser only allows scripts to close tabs opened by another script. Close this leader tab manually."), 100);
});
addEventListener("pagehide", () => machine?.leave(), { once: true });
</script>
</body>
</html>